🐛 Improved error message for unauthorized YouTube embeds (#16374)

refs TryGhost/Ghost#16048

- When attempting to embed a Youtube video that has had embedding
disabled by its owner/author, Ghost displayed a generic error message
that didn't indicate the reason for the failed emebed.
- This change updated the error message when Youtube (or any provider)
returns 401: Unauthorized to indicate that the owner of the resource has
explicitly disabled embedding.
This commit is contained in:
Chris Raible 2023-05-04 16:04:58 -07:00 committed by GitHub
parent 848b2d82a1
commit 27e4523aec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 72 additions and 10 deletions

View File

@ -578,3 +578,6 @@ add|ember-template-lint|no-action|25|125|25|125|ba0f8b6ba2697f1b071200d1a3dae9c3
add|ember-template-lint|no-action|48|46|48|46|2f3118270fbb1ff6e5da6b0d482ccd21e69df3b5|1681862400000|1692230400000|1697414400000|app/components/modal-post-history.hbs add|ember-template-lint|no-action|48|46|48|46|2f3118270fbb1ff6e5da6b0d482ccd21e69df3b5|1681862400000|1692230400000|1697414400000|app/components/modal-post-history.hbs
add|ember-template-lint|require-valid-alt-text|13|20|13|20|41dff435a7aba8088be689c6d9b1e76bef081d17|1682035200000|1692403200000|1697587200000|app/components/modal-post-history.hbs add|ember-template-lint|require-valid-alt-text|13|20|13|20|41dff435a7aba8088be689c6d9b1e76bef081d17|1682035200000|1692403200000|1697587200000|app/components/modal-post-history.hbs
add|ember-template-lint|require-valid-alt-text|20|20|20|20|bc0bb4f51567cea7289bfcb30d02932f0f57d0d9|1682035200000|1692403200000|1697587200000|app/components/modal-post-history.hbs add|ember-template-lint|require-valid-alt-text|20|20|20|20|bc0bb4f51567cea7289bfcb30d02932f0f57d0d9|1682035200000|1692403200000|1697587200000|app/components/modal-post-history.hbs
add|ember-template-lint|no-action|55|71|55|71|76726a13a086d82dab219df12e86db1773a9de32|1678147200000|1688511600000|1693695600000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|56|85|56|85|bb78ad59bc384ea0de5e9459da9d85f1735ce141|1678147200000|1688511600000|1693695600000|lib/koenig-editor/addon/components/koenig-card-embed.hbs
add|ember-template-lint|no-action|57|38|57|38|3ad187464ff78253a0ea4dd17dcfcf0423f66864|1678147200000|1688511600000|1693695600000|lib/koenig-editor/addon/components/koenig-card-embed.hbs

View File

@ -40,6 +40,15 @@
<div class="miw-100 pa2 ba br2 b--lightgrey-d1 flex items-center justify-center bg-whitegrey-l2 f6 lh-title h10"> <div class="miw-100 pa2 ba br2 b--lightgrey-d1 flex items-center justify-center bg-whitegrey-l2 f6 lh-title h10">
&nbsp;<div class="ghost-spinner spinner-blue"></div>&nbsp; &nbsp;<div class="ghost-spinner spinner-blue"></div>&nbsp;
</div> </div>
{{else if this.isUnauthorized}}
<div class="miw-100 flex flex-row pa2 pl3 ba br2 b--red-l3 red bg-error-red f7 fw4 lh-title h10 items-center">
<span class="mr3">The owner of this URL has disabled embedding.</span>
<button type="button" class="red-d2 mr3 fw6 hover-red" {{action "retry"}}><span class="underline">Retry</span></button>
<button type="button" class="red-d2 mr-auto fw6 underline hover-red" {{action "insertAsLink"}}><span class="underline">Paste URL as link</span></button>
<button type="button" {{action this.deleteCard}} class="nudge-right--2">
{{svg-jar "close" class="w3 stroke-red-l3"}}
</button>
</div>
{{else if this.hasError}} {{else if this.hasError}}
<div class="miw-100 flex flex-row pa2 pl3 ba br2 b--red-l3 red bg-error-red f7 fw4 lh-title h10 items-center"> <div class="miw-100 flex flex-row pa2 pl3 ba br2 b--red-l3 red bg-error-red f7 fw4 lh-title h10 items-center">
<span class="mr3">There was an error when parsing the URL.</span> <span class="mr3">There was an error when parsing the URL.</span>

View File

@ -6,6 +6,7 @@ import {NO_CURSOR_MOVEMENT} from './koenig-editor';
import {action, computed, set} from '@ember/object'; import {action, computed, set} from '@ember/object';
import {utils as ghostHelperUtils} from '@tryghost/helpers'; import {utils as ghostHelperUtils} from '@tryghost/helpers';
import {isBlank} from '@ember/utils'; import {isBlank} from '@ember/utils';
import {isUnauthorizedError} from 'ember-ajax/errors';
import {run} from '@ember/runloop'; import {run} from '@ember/runloop';
import {task} from 'ember-concurrency'; import {task} from 'ember-concurrency';
@ -23,6 +24,7 @@ export default class KoenigCardEmbed extends Component {
// internal properties // internal properties
hasError = false; hasError = false;
isUnauthorized = false;
// closure actions // closure actions
selectCard() {} selectCard() {}
@ -114,6 +116,7 @@ export default class KoenigCardEmbed extends Component {
@action @action
retry() { retry() {
this.set('hasError', false); this.set('hasError', false);
this.set('isUnauthorized', false);
} }
@action @action
@ -208,6 +211,9 @@ export default class KoenigCardEmbed extends Component {
return; return;
} }
this.set('hasError', true); this.set('hasError', true);
if (isUnauthorizedError(err)) {
this.set('isUnauthorized', true);
}
} }
}).drop()) }).drop())
convertUrl; convertUrl;

View File

@ -1,4 +1,4 @@
const {extract} = require('oembed-parser'); const {extract} = require('@extractus/oembed-extractor');
const logging = require('@tryghost/logging'); const logging = require('@tryghost/logging');
/** /**

View File

@ -60,6 +60,7 @@
"dependencies": { "dependencies": {
"@sentry/node": "7.50.0", "@sentry/node": "7.50.0",
"@tryghost/adapter-base-cache": "0.1.5", "@tryghost/adapter-base-cache": "0.1.5",
"@extractus/oembed-extractor": "^3.1.8",
"@tryghost/adapter-cache-redis": "0.0.0", "@tryghost/adapter-cache-redis": "0.0.0",
"@tryghost/adapter-manager": "0.0.0", "@tryghost/adapter-manager": "0.0.0",
"@tryghost/admin-api-schema": "4.3.0", "@tryghost/admin-api-schema": "4.3.0",
@ -202,7 +203,6 @@
"mysql2": "3.2.0", "mysql2": "3.2.0",
"nconf": "0.12.0", "nconf": "0.12.0",
"node-jose": "2.2.0", "node-jose": "2.2.0",
"oembed-parser": "1.4.9",
"path-match": "1.2.4", "path-match": "1.2.4",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"rss": "1.2.2", "rss": "1.2.2",

View File

@ -58,6 +58,37 @@ describe('Oembed API', function () {
should.exist(res.body.html); should.exist(res.body.html);
}); });
it('errors with a useful message when embedding is disabled', async function () {
const requestMock = nock('https://www.youtube.com')
.get('/oembed')
.query(true)
.reply(401, {
errors: [
{
message: 'Authorisation error, cannot read oembed.',
context: 'URL contains a private resource.',
type: 'UnauthorizedError',
details: null,
property: null,
help: null,
code: null,
id: 'c51228a0-921a-11ed-8abe-6babfda4d18a',
ghostErrorCode: null
}
]
});
const res = await request.get(localUtils.API.getApiQuery('oembed/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DE5yFcdPAGv0'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(401);
requestMock.isDone().should.be.true();
should.exist(res.body.errors);
res.body.errors[0].context.should.match(/URL contains a private resource/i);
});
describe('type: bookmark', function () { describe('type: bookmark', function () {
it('can fetch a bookmark with ?type=bookmark', async function () { it('can fetch a bookmark with ?type=bookmark', async function () {
const pageMock = nock('http://example.com') const pageMock = nock('http://example.com')

View File

@ -1,7 +1,7 @@
const errors = require('@tryghost/errors'); const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl'); const tpl = require('@tryghost/tpl');
const logging = require('@tryghost/logging'); const logging = require('@tryghost/logging');
const {extract, hasProvider} = require('oembed-parser'); const {extract, hasProvider} = require('@extractus/oembed-extractor');
const cheerio = require('cheerio'); const cheerio = require('cheerio');
const _ = require('lodash'); const _ = require('lodash');
const charset = require('charset'); const charset = require('charset');
@ -10,7 +10,8 @@ const iconv = require('iconv-lite');
const messages = { const messages = {
noUrlProvided: 'No url provided.', noUrlProvided: 'No url provided.',
insufficientMetadata: 'URL contains insufficient metadata.', insufficientMetadata: 'URL contains insufficient metadata.',
unknownProvider: 'No provider found for supplied URL.' unknownProvider: 'No provider found for supplied URL.',
unauthorized: 'URL contains a private resource.'
}; };
/** /**
@ -53,7 +54,7 @@ const findUrlWithProvider = (url) => {
/** /**
* @typedef {object} ICustomProvider * @typedef {object} ICustomProvider
* @prop {(url: URL) => Promise<boolean>} canSupportRequest * @prop {(url: URL) => Promise<boolean>} canSupportRequest
* @prop {(url: URL, externalRequest: IExternalRequest) => Promise<import('oembed-parser').OembedData>} getOEmbedData * @prop {(url: URL, externalRequest: IExternalRequest) => Promise<import('@extractus/oembed-extractor').OembedData>} getOEmbedData
*/ */
class OEmbed { class OEmbed {
@ -97,9 +98,15 @@ class OEmbed {
try { try {
return await extract(url); return await extract(url);
} catch (err) { } catch (err) {
throw new errors.InternalServerError({ if (err.message === 'Request failed with error code 401') {
message: err.message throw new errors.UnauthorizedError({
}); message: messages.unauthorized
});
} else {
throw new errors.InternalServerError({
message: err.message
});
}
} }
} }

View File

@ -56,7 +56,6 @@
"intl-messageformat", "intl-messageformat",
"moment", "moment",
"moment-timezone", "moment-timezone",
"oembed-parser",
"simple-dom", "simple-dom",
"ember-drag-drop", "ember-drag-drop",
"normalize.css", "normalize.css",

View File

@ -2589,6 +2589,13 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d"
integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A== integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==
"@extractus/oembed-extractor@^3.1.8":
version "3.1.8"
resolved "https://registry.yarnpkg.com/@extractus/oembed-extractor/-/oembed-extractor-3.1.8.tgz#79ea7ed65c7688bdf9ee673a0ac5aa122cef5e4e"
integrity sha512-k6p8des8ISJY2fuuQDyiUOTlcuPOzWETM2ewF1aywFNSS3EvgGWRwoMsvBKhCrLuvb8NyEKDAJB0SWgt0L793w==
dependencies:
cross-fetch "^3.1.5"
"@faker-js/faker@7.6.0": "@faker-js/faker@7.6.0":
version "7.6.0" version "7.6.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07" resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.6.0.tgz#9ea331766084288634a9247fcd8b84f16ff4ba07"
@ -11749,7 +11756,7 @@ cron-validate@1.4.5, cron-validate@^1.4.1:
dependencies: dependencies:
yup "0.32.9" yup "0.32.9"
cross-fetch@3.1.5, cross-fetch@^3.1.4: cross-fetch@3.1.5, cross-fetch@^3.1.4, cross-fetch@^3.1.5:
version "3.1.5" version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==