0
Fork 0
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:
Kevin Ansfield 2018-06-05 15:49:23 +01:00
parent d506e86f76
commit ca20f3a6b0
9 changed files with 209 additions and 1 deletions

View file

@ -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
View 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;

View file

@ -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

View file

@ -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": {

View file

@ -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;
};

View 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();
});
});
});
});

View 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();
});
});
});
});

View file

@ -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",

View file

@ -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"