mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Added /oembed API endpoint
refs https://github.com/TryGhost/Ghost/issues/9623 - add `oembed-parser` module for checking provider availability for a url and fetching data from the provider - require it in the `overrides.js` file before the general Promise override so that the `promise-wrt` sub-dependency doesn't attempt to extend the Bluebird promise implementation - add `/oembed` authenticated endpoint - takes `?url=` query parameter to match against known providers - adds safeguard against oembed-parser's providers list not recognising http+https and www+non-www - responds with `ValidationError` if no provider is found - responds with oembed response from matched provider's oembed endpoint if match is found
This commit is contained in:
parent
d506e86f76
commit
ca20f3a6b0
9 changed files with 209 additions and 1 deletions
|
@ -29,6 +29,7 @@ var _ = require('lodash'),
|
|||
exporter = require('../data/export'),
|
||||
slack = require('./slack'),
|
||||
webhooks = require('./webhooks'),
|
||||
oembed = require('./oembed'),
|
||||
|
||||
http,
|
||||
addHeaders,
|
||||
|
@ -317,7 +318,8 @@ module.exports = {
|
|||
themes: themes,
|
||||
invites: invites,
|
||||
redirects: redirects,
|
||||
webhooks: webhooks
|
||||
webhooks: webhooks,
|
||||
oembed: oembed
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
47
core/server/api/oembed.js
Normal file
47
core/server/api/oembed.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const common = require('../lib/common');
|
||||
const {extract, hasProvider} = require('oembed-parser');
|
||||
const Promise = require('bluebird');
|
||||
|
||||
let oembed = {
|
||||
read(options) {
|
||||
let {url} = options;
|
||||
|
||||
if (!url || !url.trim()) {
|
||||
return Promise.reject(new common.errors.BadRequestError({
|
||||
message: common.i18n.t('errors.api.oembed.noUrlProvided')
|
||||
}));
|
||||
}
|
||||
|
||||
// build up a list of URL variations to test against because the oembed
|
||||
// providers list is not always up to date with scheme or www vs non-www
|
||||
let base = url.replace(/https?:\/\/(?:www\.)?/, '');
|
||||
let testUrls = [
|
||||
`http://${base}`,
|
||||
`https://${base}`,
|
||||
`http://www.${base}`,
|
||||
`https://www.${base}`
|
||||
];
|
||||
let provider;
|
||||
for (let testUrl of testUrls) {
|
||||
provider = hasProvider(testUrl);
|
||||
if (provider) {
|
||||
url = testUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.oembed.unknownProvider')
|
||||
}));
|
||||
}
|
||||
|
||||
return extract(url).catch((err) => {
|
||||
return Promise.reject(new common.errors.InternalServerError({
|
||||
message: err.message
|
||||
}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = oembed;
|
|
@ -1,5 +1,13 @@
|
|||
const moment = require('moment-timezone');
|
||||
|
||||
/**
|
||||
* oembed-parser uses promise-wtf to extend the global Promise with .finally
|
||||
* - require it before global Bluebird Promise override so that promise-wtf
|
||||
* doesn't error due to Bluebird's Promise already having a .finally
|
||||
* - https://github.com/ndaidong/promise-wtf/issues/25
|
||||
*/
|
||||
const {extract, hasProvider} = require('oembed-parser'); // eslint-disable-line
|
||||
|
||||
/**
|
||||
* force UTC
|
||||
* - you can require moment or moment-timezone, both is configured to UTC
|
||||
|
|
|
@ -424,6 +424,10 @@
|
|||
},
|
||||
"webhooks": {
|
||||
"webhookAlreadyExists": "A webhook for requested event with supplied target_url already exists."
|
||||
},
|
||||
"oembed": {
|
||||
"noUrlProvided": "No url provided.",
|
||||
"unknownProvider": "No provider found for supplied URL."
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
|
|
|
@ -206,5 +206,8 @@ module.exports = function apiRoutes() {
|
|||
apiRouter.post('/webhooks', mw.authenticatePrivate, api.http(api.webhooks.add));
|
||||
apiRouter.del('/webhooks/:id', mw.authenticatePrivate, api.http(api.webhooks.destroy));
|
||||
|
||||
// ## Oembed (fetch response from oembed provider)
|
||||
apiRouter.get('/oembed', mw.authenticatePrivate, api.http(api.oembed.read));
|
||||
|
||||
return apiRouter;
|
||||
};
|
||||
|
|
63
core/test/functional/routes/api/oembed_spec.js
Normal file
63
core/test/functional/routes/api/oembed_spec.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
const config = require('../../../../../core/server/config');
|
||||
const nock = require('nock');
|
||||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const testUtils = require('../../../utils');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
|
||||
describe('Oembed API', function () {
|
||||
let accesstoken = '', ghostServer, request;
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then((_ghostServer) => {
|
||||
ghostServer = _ghostServer;
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(() => {
|
||||
return testUtils.doAuth(request);
|
||||
})
|
||||
.then((token) => {
|
||||
accesstoken = token;
|
||||
});
|
||||
});
|
||||
|
||||
describe('success', function () {
|
||||
it('can fetch an embed', function (done) {
|
||||
let requestMock = nock('https://www.youtube.com')
|
||||
.get('/oembed')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
html: '<iframe width="480" height="270" src="https://www.youtube.com/embed/E5yFcdPAGv0?feature=oembed" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>',
|
||||
thumbnail_width: 480,
|
||||
width: 480,
|
||||
author_url: 'https://www.youtube.com/user/gorillaz',
|
||||
height: 270,
|
||||
thumbnail_height: 360,
|
||||
provider_name: 'YouTube',
|
||||
title: 'Gorillaz - Humility (Official Video)',
|
||||
provider_url: 'https://www.youtube.com/',
|
||||
author_name: 'Gorillaz',
|
||||
version: '1.0',
|
||||
thumbnail_url: 'https://i.ytimg.com/vi/E5yFcdPAGv0/hqdefault.jpg',
|
||||
type: 'video'
|
||||
});
|
||||
|
||||
request.get(testUtils.API.getApiQuery('oembed/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DE5yFcdPAGv0'))
|
||||
.set('Authorization', 'Bearer ' + accesstoken)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
requestMock.isDone().should.be.true();
|
||||
should.exist(res.body.html);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
60
core/test/unit/api/oembed_spec.js
Normal file
60
core/test/unit/api/oembed_spec.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const common = require('../../../server/lib/common');
|
||||
const nock = require('nock');
|
||||
const OembedAPI = require('../../../server/api/oembed');
|
||||
const should = require('should');
|
||||
|
||||
describe('API: oembed', function () {
|
||||
describe('fn: read', function () {
|
||||
// https://oembed.com/providers.json only has schemes for https://reddit.com
|
||||
it('finds match for unlisted http scheme', function (done) {
|
||||
let requestMock = nock('https://www.reddit.com')
|
||||
.get('/oembed')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
html: 'test'
|
||||
});
|
||||
|
||||
OembedAPI.read({url: 'http://www.reddit.com/r/pics/comments/8qi5oq/breathtaking_picture_of_jupiter_with_its_moon_io/'})
|
||||
.then((results) => {
|
||||
should.exist(results);
|
||||
should.exist(results.html);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('returns error for missing url', function (done) {
|
||||
OembedAPI.read({url: ''})
|
||||
.then(() => {
|
||||
done(new Error('Fetch oembed without url should error'));
|
||||
}).catch((err) => {
|
||||
(err instanceof common.errors.BadRequestError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error for unsupported provider', function (done) {
|
||||
OembedAPI.read({url: 'http://example.com/unknown'})
|
||||
.then(() => {
|
||||
done(new Error('Fetch oembed with unknown url provider should error'));
|
||||
}).catch((err) => {
|
||||
(err instanceof common.errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error for fetch failure', function (done) {
|
||||
let requestMock = nock('https://www.youtube.com')
|
||||
.get('/oembed')
|
||||
.query(true)
|
||||
.reply(500);
|
||||
|
||||
OembedAPI.read({url: 'https://www.youtube.com/watch?v=E5yFcdPAGv0'})
|
||||
.then(() => {
|
||||
done(new Error('Fetch oembed with external failure should error'));
|
||||
}).catch((err) => {
|
||||
(err instanceof common.errors.InternalServerError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -80,6 +80,7 @@
|
|||
"netjet": "1.3.0",
|
||||
"nodemailer": "0.7.1",
|
||||
"oauth2orize": "1.11.0",
|
||||
"oembed-parser": "1.1.0",
|
||||
"passport": "0.4.0",
|
||||
"passport-http-bearer": "1.0.1",
|
||||
"passport-oauth2-client-password": "0.1.2",
|
||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -480,6 +480,10 @@ bcryptjs@2.4.3:
|
|||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
|
||||
|
||||
bellajs@^7.2.2:
|
||||
version "7.2.2"
|
||||
resolved "https://registry.yarnpkg.com/bellajs/-/bellajs-7.2.2.tgz#dc6f8c13acb248dc2ef1b46d01286a16ca31025a"
|
||||
|
||||
bignumber.js@4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.0.4.tgz#7c40f5abcd2d6623ab7b99682ee7db81b11889a4"
|
||||
|
@ -4026,6 +4030,10 @@ node-abi@^2.2.0:
|
|||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-fetch@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
|
||||
|
||||
node-gyp@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
|
||||
|
@ -4238,6 +4246,14 @@ object.pick@^1.2.0, object.pick@^1.3.0:
|
|||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
oembed-parser@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/oembed-parser/-/oembed-parser-1.1.0.tgz#1e61c2e89c200d9bbc46fceee1b44698b5011450"
|
||||
dependencies:
|
||||
bellajs "^7.2.2"
|
||||
node-fetch "^2.1.2"
|
||||
promise-wtf "^1.2.4"
|
||||
|
||||
on-finished@^2.3.0, on-finished@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||
|
@ -4763,6 +4779,10 @@ progress@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
|
||||
|
||||
promise-wtf@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/promise-wtf/-/promise-wtf-1.2.4.tgz#8cbdd31ea10dee074fbb6387cbc96e413993376c"
|
||||
|
||||
propagate@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/propagate/-/propagate-1.0.0.tgz#00c2daeedda20e87e3782b344adba1cddd6ad709"
|
||||
|
|
Loading…
Add table
Reference in a new issue