ghost 4.22.1 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc.json +24 -0
- package/.eslintrc.js +6 -0
- package/Gruntfile.js +1 -1
- package/content/public/README.md +3 -0
- package/core/boot.js +20 -12
- package/core/built/assets/{chunk.3.1148677ff3b78e5aeaee.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
- package/core/built/assets/{ghost-dark-684ad238e1a858c7cb5be6988de7c6f5.css → ghost-dark-42cf6e0c730578940ec069bda45aea41.css} +1 -1
- package/core/built/assets/{ghost.min-f7037eca328f4d4eb99f0309c19c9bae.js → ghost.min-cccc107e881b74c7aaf1a73e1e5e0dee.js} +189 -143
- package/core/built/assets/{ghost.min-66e08535f8bb797a8c40e0a2b31f1e9e.css → ghost.min-fcf6a0738421f86c47c55f20d00c5ba9.css} +1 -1
- package/core/built/assets/icons/powered-by-tenor.svg +35 -0
- package/core/built/assets/icons/tenor.svg +7 -0
- package/core/built/assets/{vendor.min-7c8fdd90f7ecd2e94328a07ea3b64608.js → vendor.min-c9002845b6c30ac978abdadde9f33d7c.js} +8189 -7601
- package/core/frontend/apps/amp/lib/views/amp.hbs +104 -0
- package/core/frontend/apps/private-blogging/lib/router.js +1 -1
- package/core/frontend/services/card-assets/service.js +21 -13
- package/core/frontend/services/routing/CollectionRouter.js +4 -5
- package/core/frontend/services/routing/EmailRouter.js +1 -1
- package/core/frontend/services/routing/ParentRouter.js +0 -8
- package/core/frontend/services/routing/PreviewRouter.js +1 -1
- package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
- package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
- package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
- package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
- package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
- package/core/frontend/services/routing/router-manager.js +7 -2
- package/core/frontend/services/rss/generate-feed.js +2 -1
- package/core/frontend/src/cards/css/bookmark.css +66 -48
- package/core/frontend/src/cards/css/button.css +30 -0
- package/core/frontend/src/cards/css/callout.css +50 -0
- package/core/frontend/src/cards/css/gallery.css +8 -13
- package/core/frontend/src/cards/css/nft.css +94 -0
- package/core/frontend/src/cards/css/toggle.css +47 -0
- package/core/frontend/src/cards/js/toggle.js +16 -0
- package/core/frontend/web/middleware/serve-public-file.js +14 -8
- package/core/frontend/web/routes.js +0 -1
- package/core/frontend/web/site.js +15 -12
- package/core/server/adapters/storage/LocalFilesStorage.js +17 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +1 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +2 -1
- package/core/server/adapters/storage/LocalStorageBase.js +30 -5
- package/core/server/api/canary/authentication.js +1 -1
- package/core/server/api/canary/files.js +19 -0
- package/core/server/api/canary/index.js +4 -0
- package/core/server/api/canary/media.js +25 -5
- package/core/server/api/canary/oembed.js +3 -0
- package/core/server/api/canary/utils/serializers/input/index.js +4 -0
- package/core/server/api/canary/utils/serializers/input/media.js +8 -0
- package/core/server/api/canary/utils/serializers/output/config.js +21 -14
- package/core/server/api/canary/utils/serializers/output/files.js +27 -0
- package/core/server/api/canary/utils/serializers/output/index.js +4 -0
- package/core/server/api/canary/utils/serializers/output/media.js +9 -0
- package/core/server/api/canary/utils/validators/input/files.js +7 -0
- package/core/server/api/canary/utils/validators/input/index.js +4 -0
- package/core/server/api/canary/utils/validators/input/media.js +4 -0
- package/core/server/api/v2/authentication.js +1 -1
- package/core/server/api/v3/authentication.js +1 -1
- package/core/server/data/db/connection.js +7 -0
- package/core/server/data/importer/importers/data/data-importer.js +3 -3
- package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
- package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
- package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
- package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
- package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
- package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
- package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
- package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
- package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +58 -0
- package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
- package/core/server/data/schema/fixtures/index.js +8 -2
- package/core/server/services/mega/post-email-serializer.js +5 -1
- package/core/server/services/mega/segment-parser.js +1 -2
- package/core/server/services/mega/template.js +69 -1
- package/core/server/services/nft-oembed.js +57 -0
- package/core/server/services/oembed.js +161 -126
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/stripe/index.js +4 -2
- package/core/server/services/url/Resource.js +1 -1
- package/core/server/services/url/Resources.js +36 -23
- package/core/server/services/url/UrlGenerator.js +23 -20
- package/core/server/services/url/UrlService.js +123 -21
- package/core/server/services/url/Urls.js +7 -2
- package/core/server/services/url/index.js +9 -1
- package/core/server/web/admin/app.js +6 -6
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +1 -1
- package/core/server/web/api/canary/admin/app.js +4 -4
- package/core/server/web/api/canary/admin/middleware.js +6 -6
- package/core/server/web/api/canary/admin/routes.js +20 -5
- package/core/server/web/api/canary/content/app.js +4 -4
- package/core/server/web/api/canary/content/middleware.js +3 -3
- package/core/server/web/api/middleware/cors.js +7 -7
- package/core/server/web/api/v2/admin/app.js +4 -4
- package/core/server/web/api/v2/admin/middleware.js +6 -6
- package/core/server/web/api/v2/admin/routes.js +5 -5
- package/core/server/web/api/v2/content/app.js +4 -4
- package/core/server/web/api/v2/content/middleware.js +3 -3
- package/core/server/web/api/v3/admin/app.js +4 -4
- package/core/server/web/api/v3/admin/middleware.js +6 -6
- package/core/server/web/api/v3/admin/routes.js +5 -5
- package/core/server/web/api/v3/content/app.js +4 -4
- package/core/server/web/api/v3/content/middleware.js +3 -3
- package/core/server/web/members/app.js +7 -7
- package/core/server/web/oauth/app.js +1 -1
- package/core/server/web/parent/app.js +2 -3
- package/core/server/web/parent/frontend.js +1 -1
- package/core/server/web/shared/index.js +2 -2
- package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/error-handler.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/index.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/maintenance.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
- package/core/shared/config/defaults.json +10 -2
- package/core/shared/config/helpers.js +44 -0
- package/core/shared/config/loader.js +1 -1
- package/core/shared/config/overrides.json +2 -2
- package/core/shared/labs.js +8 -1
- package/loggingrc.js +19 -20
- package/package.json +35 -35
- package/urls.json +597 -0
- package/yarn.lock +655 -339
- package/core/server/data/schema/fixtures/utils.js +0 -321
- package/core/server/web/parent/vhost-utils.js +0 -39
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const Promise = require('bluebird');
|
|
2
1
|
const errors = require('@tryghost/errors');
|
|
3
2
|
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const logging = require('@tryghost/logging');
|
|
4
4
|
const {extract, hasProvider} = require('oembed-parser');
|
|
5
5
|
const cheerio = require('cheerio');
|
|
6
6
|
const _ = require('lodash');
|
|
@@ -11,6 +11,10 @@ const messages = {
|
|
|
11
11
|
insufficientMetadata: 'URL contains insufficient metadata.'
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} url
|
|
16
|
+
* @returns {{url: string, provider: boolean}}
|
|
17
|
+
*/
|
|
14
18
|
const findUrlWithProvider = (url) => {
|
|
15
19
|
let provider;
|
|
16
20
|
|
|
@@ -44,6 +48,12 @@ const findUrlWithProvider = (url) => {
|
|
|
44
48
|
* @typedef {(url: string, config: Object) => Promise} IExternalRequest
|
|
45
49
|
*/
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {object} ICustomProvider
|
|
53
|
+
* @prop {(url: URL) => Promise<boolean>} canSupportRequest
|
|
54
|
+
* @prop {(url: URL, externalRequest: IExternalRequest) => Promise<import('oembed-parser').OembedData>} getOEmbedData
|
|
55
|
+
*/
|
|
56
|
+
|
|
47
57
|
class OEmbed {
|
|
48
58
|
/**
|
|
49
59
|
*
|
|
@@ -53,34 +63,51 @@ class OEmbed {
|
|
|
53
63
|
*/
|
|
54
64
|
constructor({config, externalRequest}) {
|
|
55
65
|
this.config = config;
|
|
56
|
-
|
|
66
|
+
|
|
67
|
+
/** @type {IExternalRequest} */
|
|
68
|
+
this.externalRequest = async (url, requestConfig) => {
|
|
69
|
+
if (this.isIpOrLocalhost(url)) {
|
|
70
|
+
return this.unknownProvider(url);
|
|
71
|
+
}
|
|
72
|
+
const response = await externalRequest(url, requestConfig);
|
|
73
|
+
if (this.isIpOrLocalhost(response.url)) {
|
|
74
|
+
return this.unknownProvider(url);
|
|
75
|
+
}
|
|
76
|
+
return response;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @type {ICustomProvider[]} */
|
|
80
|
+
this.customProviders = [];
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
/**
|
|
84
|
+
* @param {ICustomProvider} provider
|
|
85
|
+
*/
|
|
86
|
+
registerProvider(provider) {
|
|
87
|
+
this.customProviders.push(provider);
|
|
64
88
|
}
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} url
|
|
92
|
+
*/
|
|
93
|
+
async unknownProvider(url) {
|
|
94
|
+
throw new errors.ValidationError({
|
|
95
|
+
message: tpl(messages.unknownProvider),
|
|
96
|
+
context: url
|
|
71
97
|
});
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} url
|
|
102
|
+
*/
|
|
103
|
+
async knownProvider(url) {
|
|
104
|
+
try {
|
|
105
|
+
return await extract(url);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw new errors.InternalServerError({
|
|
108
|
+
message: err.message
|
|
109
|
+
});
|
|
110
|
+
}
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
async fetchBookmarkData(url) {
|
|
@@ -97,19 +124,11 @@ class OEmbed {
|
|
|
97
124
|
|
|
98
125
|
let scraperResponse;
|
|
99
126
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const response = await this.externalRequest(url, {cookieJar});
|
|
127
|
+
const cookieJar = new CookieJar();
|
|
128
|
+
const response = await this.externalRequest(url, {cookieJar});
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} else {
|
|
107
|
-
const html = response.body;
|
|
108
|
-
scraperResponse = await metascraper({html, url});
|
|
109
|
-
}
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return Promise.reject(err);
|
|
112
|
-
}
|
|
130
|
+
const html = response.body;
|
|
131
|
+
scraperResponse = await metascraper({html, url});
|
|
113
132
|
|
|
114
133
|
const metadata = Object.assign({}, scraperResponse, {
|
|
115
134
|
thumbnail: scraperResponse.image,
|
|
@@ -119,20 +138,25 @@ class OEmbed {
|
|
|
119
138
|
delete metadata.image;
|
|
120
139
|
delete metadata.logo;
|
|
121
140
|
|
|
122
|
-
if (metadata.title) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
url
|
|
126
|
-
metadata
|
|
141
|
+
if (!metadata.title) {
|
|
142
|
+
throw new errors.ValidationError({
|
|
143
|
+
message: tpl(messages.insufficientMetadata),
|
|
144
|
+
context: url
|
|
127
145
|
});
|
|
128
146
|
}
|
|
129
147
|
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
return {
|
|
149
|
+
version: '1.0',
|
|
150
|
+
type: 'bookmark',
|
|
151
|
+
url,
|
|
152
|
+
metadata
|
|
153
|
+
};
|
|
134
154
|
}
|
|
135
155
|
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} url
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
136
160
|
isIpOrLocalhost(url) {
|
|
137
161
|
try {
|
|
138
162
|
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
@@ -163,14 +187,7 @@ class OEmbed {
|
|
|
163
187
|
*
|
|
164
188
|
* @returns {Promise<Object>}
|
|
165
189
|
*/
|
|
166
|
-
fetchOembedData(_url, cardType) {
|
|
167
|
-
// parse the url then validate the protocol and host to make sure it's
|
|
168
|
-
// http(s) and not an IP address or localhost to avoid potential access to
|
|
169
|
-
// internal network endpoints
|
|
170
|
-
if (this.isIpOrLocalhost(_url)) {
|
|
171
|
-
return this.unknownProvider();
|
|
172
|
-
}
|
|
173
|
-
|
|
190
|
+
async fetchOembedData(_url, cardType) {
|
|
174
191
|
// check against known oembed list
|
|
175
192
|
let {url, provider} = findUrlWithProvider(_url);
|
|
176
193
|
if (provider) {
|
|
@@ -180,88 +197,81 @@ class OEmbed {
|
|
|
180
197
|
// url not in oembed list so fetch it in case it's a redirect or has a
|
|
181
198
|
// <link rel="alternate" type="application/json+oembed"> element
|
|
182
199
|
const cookieJar = new CookieJar();
|
|
183
|
-
|
|
200
|
+
const pageResponse = await this.externalRequest(url, {
|
|
184
201
|
method: 'GET',
|
|
185
202
|
timeout: 2 * 1000,
|
|
186
203
|
followRedirect: true,
|
|
187
204
|
cookieJar
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
205
|
+
});
|
|
206
|
+
// url changed after fetch, see if we were redirected to a known oembed
|
|
207
|
+
if (pageResponse.url !== url) {
|
|
208
|
+
({url, provider} = findUrlWithProvider(pageResponse.url));
|
|
209
|
+
if (provider) {
|
|
210
|
+
return this.knownProvider(url);
|
|
195
211
|
}
|
|
212
|
+
}
|
|
196
213
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
// check for <link rel="alternate" type="application/json+oembed"> element
|
|
215
|
+
let oembedUrl;
|
|
216
|
+
try {
|
|
217
|
+
oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return this.unknownProvider(url);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (oembedUrl) {
|
|
223
|
+
// for standard WP oembed's we want to insert a bookmark card rather than their blockquote+script
|
|
224
|
+
// which breaks in the editor and most Ghost themes. Only fallback if card type was not explicitly chosen
|
|
225
|
+
if (!cardType && oembedUrl.match(/wp-json\/oembed/)) {
|
|
226
|
+
return;
|
|
203
227
|
}
|
|
204
228
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
// fetch oembed response from embedded rel="alternate" url
|
|
230
|
+
const oembedResponse = await this.externalRequest(oembedUrl, {
|
|
231
|
+
method: 'GET',
|
|
232
|
+
json: true,
|
|
233
|
+
timeout: 2 * 1000,
|
|
234
|
+
followRedirect: true,
|
|
235
|
+
cookieJar
|
|
236
|
+
});
|
|
237
|
+
// validate the fetched json against the oembed spec to avoid
|
|
238
|
+
// leaking non-oembed responses
|
|
239
|
+
const body = oembedResponse.body;
|
|
240
|
+
const hasRequiredFields = body.type && body.version;
|
|
241
|
+
const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
|
|
242
|
+
|
|
243
|
+
if (hasRequiredFields && hasValidType) {
|
|
244
|
+
// extract known oembed fields from the response to limit leaking of unrecognised data
|
|
245
|
+
const knownFields = [
|
|
246
|
+
'type',
|
|
247
|
+
'version',
|
|
248
|
+
'html',
|
|
249
|
+
'url',
|
|
250
|
+
'title',
|
|
251
|
+
'width',
|
|
252
|
+
'height',
|
|
253
|
+
'author_name',
|
|
254
|
+
'author_url',
|
|
255
|
+
'provider_name',
|
|
256
|
+
'provider_url',
|
|
257
|
+
'thumbnail_url',
|
|
258
|
+
'thumbnail_width',
|
|
259
|
+
'thumbnail_height'
|
|
260
|
+
];
|
|
261
|
+
const oembed = _.pick(body, knownFields);
|
|
262
|
+
|
|
263
|
+
// ensure we have required data for certain types
|
|
264
|
+
if (oembed.type === 'photo' && !oembed.url) {
|
|
265
|
+
return;
|
|
209
266
|
}
|
|
210
|
-
|
|
211
|
-
// for standard WP oembed's we want to insert a bookmark card rather than their blockquote+script
|
|
212
|
-
// which breaks in the editor and most Ghost themes. Only fallback if card type was not explicitly chosen
|
|
213
|
-
if (!cardType && oembedUrl.match(/wp-json\/oembed/)) {
|
|
267
|
+
if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
|
|
214
268
|
return;
|
|
215
269
|
}
|
|
216
270
|
|
|
217
|
-
//
|
|
218
|
-
return
|
|
219
|
-
method: 'GET',
|
|
220
|
-
json: true,
|
|
221
|
-
timeout: 2 * 1000,
|
|
222
|
-
followRedirect: true,
|
|
223
|
-
cookieJar
|
|
224
|
-
}).then((oembedResponse) => {
|
|
225
|
-
// validate the fetched json against the oembed spec to avoid
|
|
226
|
-
// leaking non-oembed responses
|
|
227
|
-
const body = oembedResponse.body;
|
|
228
|
-
const hasRequiredFields = body.type && body.version;
|
|
229
|
-
const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
|
|
230
|
-
|
|
231
|
-
if (hasRequiredFields && hasValidType) {
|
|
232
|
-
// extract known oembed fields from the response to limit leaking of unrecognised data
|
|
233
|
-
const knownFields = [
|
|
234
|
-
'type',
|
|
235
|
-
'version',
|
|
236
|
-
'html',
|
|
237
|
-
'url',
|
|
238
|
-
'title',
|
|
239
|
-
'width',
|
|
240
|
-
'height',
|
|
241
|
-
'author_name',
|
|
242
|
-
'author_url',
|
|
243
|
-
'provider_name',
|
|
244
|
-
'provider_url',
|
|
245
|
-
'thumbnail_url',
|
|
246
|
-
'thumbnail_width',
|
|
247
|
-
'thumbnail_height'
|
|
248
|
-
];
|
|
249
|
-
const oembed = _.pick(body, knownFields);
|
|
250
|
-
|
|
251
|
-
// ensure we have required data for certain types
|
|
252
|
-
if (oembed.type === 'photo' && !oembed.url) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// return the extracted object, don't pass through the response body
|
|
260
|
-
return oembed;
|
|
261
|
-
}
|
|
262
|
-
}).catch(() => {});
|
|
271
|
+
// return the extracted object, don't pass through the response body
|
|
272
|
+
return oembed;
|
|
263
273
|
}
|
|
264
|
-
}
|
|
274
|
+
}
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
/**
|
|
@@ -271,26 +281,51 @@ class OEmbed {
|
|
|
271
281
|
* @returns {Promise<Object>}
|
|
272
282
|
*/
|
|
273
283
|
async fetchOembedDataFromUrl(url, type) {
|
|
274
|
-
let data;
|
|
275
|
-
|
|
276
284
|
try {
|
|
285
|
+
const urlObject = new URL(url);
|
|
286
|
+
|
|
287
|
+
for (const provider of this.customProviders) {
|
|
288
|
+
if (await provider.canSupportRequest(urlObject)) {
|
|
289
|
+
const result = await provider.getOEmbedData(urlObject, this.externalRequest);
|
|
290
|
+
if (result !== null) {
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// fetch only bookmark when explicitly requested
|
|
277
297
|
if (type === 'bookmark') {
|
|
278
298
|
return this.fetchBookmarkData(url);
|
|
279
299
|
}
|
|
280
300
|
|
|
281
|
-
|
|
301
|
+
// attempt to fetch oembed
|
|
302
|
+
let data = await this.fetchOembedData(url);
|
|
282
303
|
|
|
304
|
+
// fallback to bookmark when we can't get oembed
|
|
283
305
|
if (!data && !type) {
|
|
284
306
|
data = await this.fetchBookmarkData(url);
|
|
285
307
|
}
|
|
286
308
|
|
|
309
|
+
// couldn't get anything, throw a validation error
|
|
287
310
|
if (!data) {
|
|
288
|
-
|
|
311
|
+
return this.unknownProvider(url);
|
|
289
312
|
}
|
|
290
313
|
|
|
291
314
|
return data;
|
|
292
|
-
} catch (
|
|
293
|
-
|
|
315
|
+
} catch (err) {
|
|
316
|
+
// allow specific validation errors through for better error messages
|
|
317
|
+
if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// log the real error because we're going to throw a generic "Unknown provider" error
|
|
322
|
+
logging.error(new errors.GhostError({
|
|
323
|
+
message: 'Encountered error when fetching oembed',
|
|
324
|
+
err
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
// default to unknown provider to avoid leaking any app specifics
|
|
328
|
+
return this.unknownProvider(url);
|
|
294
329
|
}
|
|
295
330
|
}
|
|
296
331
|
}
|
|
@@ -16,7 +16,8 @@ module.exports = function getConfigProperties() {
|
|
|
16
16
|
stripeDirect: config.get('stripeDirect'),
|
|
17
17
|
mailgunIsConfigured: config.get('bulkEmail') && config.get('bulkEmail').mailgun,
|
|
18
18
|
emailAnalytics: config.get('emailAnalytics'),
|
|
19
|
-
hostSettings: config.get('hostSettings')
|
|
19
|
+
hostSettings: config.get('hostSettings'),
|
|
20
|
+
tenor: config.get('tenor')
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
const billingUrl = config.get('hostSettings:billing:enabled') ? config.get('hostSettings:billing:url') : '';
|
|
@@ -27,7 +27,10 @@ function configureApi() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const debouncedConfigureApi = _.debounce(
|
|
30
|
+
const debouncedConfigureApi = _.debounce(() => {
|
|
31
|
+
configureApi();
|
|
32
|
+
events.emit('services.stripe.reconfigured');
|
|
33
|
+
}, 600);
|
|
31
34
|
|
|
32
35
|
module.exports = {
|
|
33
36
|
async init() {
|
|
@@ -37,7 +40,6 @@ module.exports = {
|
|
|
37
40
|
return;
|
|
38
41
|
}
|
|
39
42
|
debouncedConfigureApi();
|
|
40
|
-
events.emit('services.stripe.reconfigured');
|
|
41
43
|
});
|
|
42
44
|
},
|
|
43
45
|
|
|
@@ -8,7 +8,7 @@ const errors = require('@tryghost/errors');
|
|
|
8
8
|
class Resource extends EventEmitter {
|
|
9
9
|
/**
|
|
10
10
|
* @param {('posts'|'pages'|'tags'|'authors')} type - of the resource
|
|
11
|
-
* @param {Object} obj - object data to
|
|
11
|
+
* @param {Object} obj - object data to store
|
|
12
12
|
*/
|
|
13
13
|
constructor(type, obj) {
|
|
14
14
|
super();
|
|
@@ -17,10 +17,16 @@ const events = require('../../lib/common/events');
|
|
|
17
17
|
* Each entry in the database will be represented by a "Resource" (see /Resource.js).
|
|
18
18
|
*/
|
|
19
19
|
class Resources {
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Object} [options.resources] - resources to initialize with instead of fetching them from the database
|
|
24
|
+
* @param {Object} [options.queue] - instance of the Queue class
|
|
25
|
+
*/
|
|
26
|
+
constructor({resources = {}, queue} = {}) {
|
|
21
27
|
this.queue = queue;
|
|
22
28
|
this.resourcesConfig = [];
|
|
23
|
-
this.data =
|
|
29
|
+
this.data = resources;
|
|
24
30
|
|
|
25
31
|
this.listeners = [];
|
|
26
32
|
}
|
|
@@ -43,38 +49,57 @@ class Resources {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
|
-
* @description
|
|
52
|
+
* @description Initialize the resource config. We currently fetch the data straight via the the model layer,
|
|
47
53
|
* but because Ghost supports multiple API versions, we have to ensure we load the correct data.
|
|
48
54
|
*
|
|
49
55
|
* @TODO: https://github.com/TryGhost/Ghost/issues/10360
|
|
50
|
-
* @private
|
|
51
56
|
*/
|
|
52
|
-
|
|
57
|
+
initResourceConfig() {
|
|
53
58
|
if (!_.isEmpty(this.resourcesConfig)) {
|
|
54
59
|
return;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const bridge = require('../../../bridge');
|
|
58
|
-
|
|
59
|
-
this.resourcesConfig = require(`./configs/${
|
|
63
|
+
const resourcesAPIVersion = bridge.getFrontendApiVersion();
|
|
64
|
+
this.resourcesConfig = require(`./configs/${resourcesAPIVersion}`);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
/**
|
|
63
|
-
* @description Helper function to
|
|
64
|
-
* events to get notified about updates/deletions/inserts.
|
|
68
|
+
* @description Helper function to initialize data fetching.
|
|
65
69
|
*/
|
|
66
70
|
fetchResources() {
|
|
67
71
|
const ops = [];
|
|
68
72
|
debug('fetchResources');
|
|
69
73
|
|
|
70
|
-
this._initResourceConfig();
|
|
71
|
-
|
|
72
74
|
// NOTE: Iterate over all resource types (posts, users etc..) and call `_fetch`.
|
|
73
75
|
_.each(this.resourcesConfig, (resourceConfig) => {
|
|
74
76
|
this.data[resourceConfig.type] = [];
|
|
75
77
|
|
|
76
78
|
// NOTE: We are querying knex directly, because the Bookshelf ORM overhead is too slow.
|
|
77
79
|
ops.push(this._fetch(resourceConfig));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return Promise.all(ops);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @description Each resource type needs to register resource/model events to get notified
|
|
87
|
+
* about updates/deletions/inserts.
|
|
88
|
+
*
|
|
89
|
+
* For example for a "tag" resource type with following configuration:
|
|
90
|
+
* events: {
|
|
91
|
+
* add: 'tag.added',
|
|
92
|
+
* update: ['tag.edited', 'tag.attached', 'tag.detached'],
|
|
93
|
+
* remove: 'tag.deleted'
|
|
94
|
+
* }
|
|
95
|
+
* there would be:
|
|
96
|
+
* 1 event listener connected to "_onResourceAdded" handler and it's 'tag.added' event
|
|
97
|
+
* 3 event listeners connected to "_onResourceUpdated" handler and it's 'tag.edited', 'tag.attached', 'tag.detached' events
|
|
98
|
+
* 1 event listener connected to "_onResourceRemoved" handler and it's 'tag.deleted' event
|
|
99
|
+
*/
|
|
100
|
+
initEvenListeners() {
|
|
101
|
+
_.each(this.resourcesConfig, (resourceConfig) => {
|
|
102
|
+
this.data[resourceConfig.type] = [];
|
|
78
103
|
|
|
79
104
|
this._listenOn(resourceConfig.events.add, (model) => {
|
|
80
105
|
return this._onResourceAdded.bind(this)(resourceConfig.type, model);
|
|
@@ -96,16 +121,6 @@ class Resources {
|
|
|
96
121
|
return this._onResourceRemoved.bind(this)(resourceConfig.type, model);
|
|
97
122
|
});
|
|
98
123
|
});
|
|
99
|
-
|
|
100
|
-
Promise.all(ops)
|
|
101
|
-
.then(() => {
|
|
102
|
-
// CASE: all resources are fetched, start the queue
|
|
103
|
-
this.queue.start({
|
|
104
|
-
event: 'init',
|
|
105
|
-
tolerance: 100,
|
|
106
|
-
requiredSubscriberCount: 1
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
/**
|
|
@@ -430,8 +445,6 @@ class Resources {
|
|
|
430
445
|
* @description Reset this class instance.
|
|
431
446
|
*
|
|
432
447
|
* Is triggered if you switch API versions.
|
|
433
|
-
*
|
|
434
|
-
* @param {Object} options
|
|
435
448
|
*/
|
|
436
449
|
reset() {
|
|
437
450
|
_.each(this.listeners, (obj) => {
|
|
@@ -33,18 +33,29 @@ const EXPANSIONS = [{
|
|
|
33
33
|
* Each router is represented by a url generator.
|
|
34
34
|
*/
|
|
35
35
|
class UrlGenerator {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object} options
|
|
38
|
+
* @param {String} options.identifier frontend router ID reference
|
|
39
|
+
* @param {String} options.filter NQL filter string
|
|
40
|
+
* @param {String} options.resourceType resource type (e.g. 'posts', 'tags')
|
|
41
|
+
* @param {String} options.permalink permalink string
|
|
42
|
+
* @param {Object} options.queue instance of the backend Queue
|
|
43
|
+
* @param {Object} options.resources instance of the backend Resources
|
|
44
|
+
* @param {Object} options.urls instance of the backend URLs (used to store the urls)
|
|
45
|
+
* @param {Number} options.position an ID of the generator
|
|
46
|
+
*/
|
|
47
|
+
constructor({identifier, filter, resourceType, permalink, queue, resources, urls, position}) {
|
|
48
|
+
this.identifier = identifier;
|
|
49
|
+
this.resourceType = resourceType;
|
|
50
|
+
this.permalink = permalink;
|
|
38
51
|
this.queue = queue;
|
|
39
52
|
this.urls = urls;
|
|
40
53
|
this.resources = resources;
|
|
41
54
|
this.uid = position;
|
|
42
55
|
|
|
43
|
-
debug('constructor', this.toString());
|
|
44
|
-
|
|
45
56
|
// CASE: routers can define custom filters, but not required.
|
|
46
|
-
if (
|
|
47
|
-
this.filter =
|
|
57
|
+
if (filter) {
|
|
58
|
+
this.filter = filter;
|
|
48
59
|
this.nql = nql(this.filter, {
|
|
49
60
|
expansions: EXPANSIONS,
|
|
50
61
|
transformer: nql.utils.mapKeyValues({
|
|
@@ -110,10 +121,10 @@ class UrlGenerator {
|
|
|
110
121
|
* @private
|
|
111
122
|
*/
|
|
112
123
|
_onInit() {
|
|
113
|
-
debug('_onInit', this.
|
|
124
|
+
debug('_onInit', this.resourceType);
|
|
114
125
|
|
|
115
126
|
// @NOTE: get the resources of my type e.g. posts.
|
|
116
|
-
const resources = this.resources.getAllByType(this.
|
|
127
|
+
const resources = this.resources.getAllByType(this.resourceType);
|
|
117
128
|
|
|
118
129
|
debug(resources.length);
|
|
119
130
|
|
|
@@ -131,7 +142,7 @@ class UrlGenerator {
|
|
|
131
142
|
debug('onAdded', this.toString());
|
|
132
143
|
|
|
133
144
|
// CASE: you are type "pages", but the incoming type is "users"
|
|
134
|
-
if (event.type !== this.
|
|
145
|
+
if (event.type !== this.resourceType) {
|
|
135
146
|
return;
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -182,8 +193,7 @@ class UrlGenerator {
|
|
|
182
193
|
* @NOTE We currently generate relative urls (https://github.com/TryGhost/Ghost/commit/7b0d5d465ba41073db0c3c72006da625fa11df32).
|
|
183
194
|
*/
|
|
184
195
|
_generateUrl(resource) {
|
|
185
|
-
|
|
186
|
-
return localUtils.replacePermalink(permalink, resource.data);
|
|
196
|
+
return localUtils.replacePermalink(this.permalink, resource.data);
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
/**
|
|
@@ -214,7 +224,7 @@ class UrlGenerator {
|
|
|
214
224
|
action: 'added:' + resource.data.id,
|
|
215
225
|
eventData: {
|
|
216
226
|
id: resource.data.id,
|
|
217
|
-
type: this.
|
|
227
|
+
type: this.resourceType
|
|
218
228
|
}
|
|
219
229
|
});
|
|
220
230
|
};
|
|
@@ -246,19 +256,12 @@ class UrlGenerator {
|
|
|
246
256
|
|
|
247
257
|
/**
|
|
248
258
|
* @description Get all urls of this url generator.
|
|
259
|
+
* NOTE: the method is only used for testing purposes at the moment.
|
|
249
260
|
* @returns {Array}
|
|
250
261
|
*/
|
|
251
262
|
getUrls() {
|
|
252
263
|
return this.urls.getByGeneratorId(this.uid);
|
|
253
264
|
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* @description Override of `toString`
|
|
257
|
-
* @returns {string}
|
|
258
|
-
*/
|
|
259
|
-
toString() {
|
|
260
|
-
return this.router.toString();
|
|
261
|
-
}
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
module.exports = UrlGenerator;
|