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.
Files changed (128) hide show
  1. package/.c8rc.json +24 -0
  2. package/.eslintrc.js +6 -0
  3. package/Gruntfile.js +1 -1
  4. package/content/public/README.md +3 -0
  5. package/core/boot.js +20 -12
  6. package/core/built/assets/{chunk.3.1148677ff3b78e5aeaee.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
  7. package/core/built/assets/{ghost-dark-684ad238e1a858c7cb5be6988de7c6f5.css → ghost-dark-42cf6e0c730578940ec069bda45aea41.css} +1 -1
  8. package/core/built/assets/{ghost.min-f7037eca328f4d4eb99f0309c19c9bae.js → ghost.min-cccc107e881b74c7aaf1a73e1e5e0dee.js} +189 -143
  9. package/core/built/assets/{ghost.min-66e08535f8bb797a8c40e0a2b31f1e9e.css → ghost.min-fcf6a0738421f86c47c55f20d00c5ba9.css} +1 -1
  10. package/core/built/assets/icons/powered-by-tenor.svg +35 -0
  11. package/core/built/assets/icons/tenor.svg +7 -0
  12. package/core/built/assets/{vendor.min-7c8fdd90f7ecd2e94328a07ea3b64608.js → vendor.min-c9002845b6c30ac978abdadde9f33d7c.js} +8189 -7601
  13. package/core/frontend/apps/amp/lib/views/amp.hbs +104 -0
  14. package/core/frontend/apps/private-blogging/lib/router.js +1 -1
  15. package/core/frontend/services/card-assets/service.js +21 -13
  16. package/core/frontend/services/routing/CollectionRouter.js +4 -5
  17. package/core/frontend/services/routing/EmailRouter.js +1 -1
  18. package/core/frontend/services/routing/ParentRouter.js +0 -8
  19. package/core/frontend/services/routing/PreviewRouter.js +1 -1
  20. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  21. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
  22. package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
  23. package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
  24. package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
  25. package/core/frontend/services/routing/router-manager.js +7 -2
  26. package/core/frontend/services/rss/generate-feed.js +2 -1
  27. package/core/frontend/src/cards/css/bookmark.css +66 -48
  28. package/core/frontend/src/cards/css/button.css +30 -0
  29. package/core/frontend/src/cards/css/callout.css +50 -0
  30. package/core/frontend/src/cards/css/gallery.css +8 -13
  31. package/core/frontend/src/cards/css/nft.css +94 -0
  32. package/core/frontend/src/cards/css/toggle.css +47 -0
  33. package/core/frontend/src/cards/js/toggle.js +16 -0
  34. package/core/frontend/web/middleware/serve-public-file.js +14 -8
  35. package/core/frontend/web/routes.js +0 -1
  36. package/core/frontend/web/site.js +15 -12
  37. package/core/server/adapters/storage/LocalFilesStorage.js +17 -0
  38. package/core/server/adapters/storage/LocalImagesStorage.js +1 -0
  39. package/core/server/adapters/storage/LocalMediaStorage.js +2 -1
  40. package/core/server/adapters/storage/LocalStorageBase.js +30 -5
  41. package/core/server/api/canary/authentication.js +1 -1
  42. package/core/server/api/canary/files.js +19 -0
  43. package/core/server/api/canary/index.js +4 -0
  44. package/core/server/api/canary/media.js +25 -5
  45. package/core/server/api/canary/oembed.js +3 -0
  46. package/core/server/api/canary/utils/serializers/input/index.js +4 -0
  47. package/core/server/api/canary/utils/serializers/input/media.js +8 -0
  48. package/core/server/api/canary/utils/serializers/output/config.js +21 -14
  49. package/core/server/api/canary/utils/serializers/output/files.js +27 -0
  50. package/core/server/api/canary/utils/serializers/output/index.js +4 -0
  51. package/core/server/api/canary/utils/serializers/output/media.js +9 -0
  52. package/core/server/api/canary/utils/validators/input/files.js +7 -0
  53. package/core/server/api/canary/utils/validators/input/index.js +4 -0
  54. package/core/server/api/canary/utils/validators/input/media.js +4 -0
  55. package/core/server/api/v2/authentication.js +1 -1
  56. package/core/server/api/v3/authentication.js +1 -1
  57. package/core/server/data/db/connection.js +7 -0
  58. package/core/server/data/importer/importers/data/data-importer.js +3 -3
  59. package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
  60. package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
  61. package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
  62. package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
  63. package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
  64. package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
  65. package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
  66. package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
  67. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +58 -0
  68. package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
  69. package/core/server/data/schema/fixtures/index.js +8 -2
  70. package/core/server/services/mega/post-email-serializer.js +5 -1
  71. package/core/server/services/mega/segment-parser.js +1 -2
  72. package/core/server/services/mega/template.js +69 -1
  73. package/core/server/services/nft-oembed.js +57 -0
  74. package/core/server/services/oembed.js +161 -126
  75. package/core/server/services/public-config/config.js +2 -1
  76. package/core/server/services/stripe/index.js +4 -2
  77. package/core/server/services/url/Resource.js +1 -1
  78. package/core/server/services/url/Resources.js +36 -23
  79. package/core/server/services/url/UrlGenerator.js +23 -20
  80. package/core/server/services/url/UrlService.js +123 -21
  81. package/core/server/services/url/Urls.js +7 -2
  82. package/core/server/services/url/index.js +9 -1
  83. package/core/server/web/admin/app.js +6 -6
  84. package/core/server/web/admin/views/default-prod.html +4 -4
  85. package/core/server/web/admin/views/default.html +4 -4
  86. package/core/server/web/api/app.js +1 -1
  87. package/core/server/web/api/canary/admin/app.js +4 -4
  88. package/core/server/web/api/canary/admin/middleware.js +6 -6
  89. package/core/server/web/api/canary/admin/routes.js +20 -5
  90. package/core/server/web/api/canary/content/app.js +4 -4
  91. package/core/server/web/api/canary/content/middleware.js +3 -3
  92. package/core/server/web/api/middleware/cors.js +7 -7
  93. package/core/server/web/api/v2/admin/app.js +4 -4
  94. package/core/server/web/api/v2/admin/middleware.js +6 -6
  95. package/core/server/web/api/v2/admin/routes.js +5 -5
  96. package/core/server/web/api/v2/content/app.js +4 -4
  97. package/core/server/web/api/v2/content/middleware.js +3 -3
  98. package/core/server/web/api/v3/admin/app.js +4 -4
  99. package/core/server/web/api/v3/admin/middleware.js +6 -6
  100. package/core/server/web/api/v3/admin/routes.js +5 -5
  101. package/core/server/web/api/v3/content/app.js +4 -4
  102. package/core/server/web/api/v3/content/middleware.js +3 -3
  103. package/core/server/web/members/app.js +7 -7
  104. package/core/server/web/oauth/app.js +1 -1
  105. package/core/server/web/parent/app.js +2 -3
  106. package/core/server/web/parent/frontend.js +1 -1
  107. package/core/server/web/shared/index.js +2 -2
  108. package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
  109. package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
  110. package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
  111. package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
  112. package/core/server/web/shared/{middlewares → middleware}/error-handler.js +0 -0
  113. package/core/server/web/shared/{middlewares → middleware}/index.js +0 -0
  114. package/core/server/web/shared/{middlewares → middleware}/maintenance.js +0 -0
  115. package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
  116. package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
  117. package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
  118. package/core/shared/config/defaults.json +10 -2
  119. package/core/shared/config/helpers.js +44 -0
  120. package/core/shared/config/loader.js +1 -1
  121. package/core/shared/config/overrides.json +2 -2
  122. package/core/shared/labs.js +8 -1
  123. package/loggingrc.js +19 -20
  124. package/package.json +35 -35
  125. package/urls.json +597 -0
  126. package/yarn.lock +655 -339
  127. package/core/server/data/schema/fixtures/utils.js +0 -321
  128. 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
- this.externalRequest = externalRequest;
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
- unknownProvider(url) {
60
- return Promise.reject(new errors.ValidationError({
61
- message: tpl(messages.unknownProvider),
62
- context: url
63
- }));
83
+ /**
84
+ * @param {ICustomProvider} provider
85
+ */
86
+ registerProvider(provider) {
87
+ this.customProviders.push(provider);
64
88
  }
65
89
 
66
- knownProvider(url) {
67
- return extract(url).catch((err) => {
68
- return Promise.reject(new errors.InternalServerError({
69
- message: err.message
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
- errorHandler(url) {
75
- return (err) => {
76
- // allow specific validation errors through for better error messages
77
- if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
78
- return Promise.reject(err);
79
- }
80
-
81
- // default to unknown provider to avoid leaking any app specifics
82
- return this.unknownProvider(url);
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
- try {
101
- const cookieJar = new CookieJar();
102
- const response = await this.externalRequest(url, {cookieJar});
127
+ const cookieJar = new CookieJar();
128
+ const response = await this.externalRequest(url, {cookieJar});
103
129
 
104
- if (this.isIpOrLocalhost(response.url)) {
105
- scraperResponse = {};
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
- return Promise.resolve({
124
- type: 'bookmark',
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 Promise.reject(new errors.ValidationError({
131
- message: tpl(messages.insufficientMetadata),
132
- context: url
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
- return this.externalRequest(url, {
200
+ const pageResponse = await this.externalRequest(url, {
184
201
  method: 'GET',
185
202
  timeout: 2 * 1000,
186
203
  followRedirect: true,
187
204
  cookieJar
188
- }).then((pageResponse) => {
189
- // url changed after fetch, see if we were redirected to a known oembed
190
- if (pageResponse.url !== url) {
191
- ({url, provider} = findUrlWithProvider(pageResponse.url));
192
- if (provider) {
193
- return this.knownProvider(url);
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
- // check for <link rel="alternate" type="application/json+oembed"> element
198
- let oembedUrl;
199
- try {
200
- oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
201
- } catch (e) {
202
- return this.unknownProvider(url);
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
- if (oembedUrl) {
206
- // make sure the linked url is not an ip address or localhost
207
- if (this.isIpOrLocalhost(oembedUrl)) {
208
- return this.unknownProvider(oembedUrl);
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
- // fetch oembed response from embedded rel="alternate" url
218
- return this.externalRequest(oembedUrl, {
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
- data = await this.fetchOembedData(url);
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
- data = await this.unknownProvider(url);
311
+ return this.unknownProvider(url);
289
312
  }
290
313
 
291
314
  return data;
292
- } catch (e) {
293
- return this.errorHandler(url);
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(configureApi, 600);
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 sotre
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
- constructor(queue) {
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 Initialise the resource config. We currently fetch the data straight via the the model layer,
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
- _initResourceConfig() {
57
+ initResourceConfig() {
53
58
  if (!_.isEmpty(this.resourcesConfig)) {
54
59
  return;
55
60
  }
56
61
 
57
62
  const bridge = require('../../../bridge');
58
- this.resourcesAPIVersion = bridge.getFrontendApiVersion();
59
- this.resourcesConfig = require(`./configs/${this.resourcesAPIVersion}`);
63
+ const resourcesAPIVersion = bridge.getFrontendApiVersion();
64
+ this.resourcesConfig = require(`./configs/${resourcesAPIVersion}`);
60
65
  }
61
66
 
62
67
  /**
63
- * @description Helper function to initialise data fetching. Each resource type needs to register resource/model
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
- constructor(router, queue, resources, urls, position) {
37
- this.router = router;
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 (this.router.getFilter()) {
47
- this.filter = this.router.getFilter();
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.router.getResourceType());
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.router.getResourceType());
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.router.getResourceType()) {
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
- const permalink = this.router.getPermalinks().getValue();
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.router.getResourceType()
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;