ghost 4.22.3 → 4.25.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 (230) hide show
  1. package/.c8rc.json +24 -0
  2. package/.eslintrc.js +39 -0
  3. package/Gruntfile.js +0 -1
  4. package/content/public/README.md +3 -0
  5. package/content/themes/casper/assets/built/casper.js +1 -1
  6. package/content/themes/casper/assets/built/casper.js.map +1 -1
  7. package/content/themes/casper/assets/built/global.css +1 -1
  8. package/content/themes/casper/assets/built/global.css.map +1 -1
  9. package/content/themes/casper/assets/built/screen.css +1 -1
  10. package/content/themes/casper/assets/built/screen.css.map +1 -1
  11. package/content/themes/casper/assets/css/global.css +6 -1
  12. package/content/themes/casper/assets/css/screen.css +32 -216
  13. package/content/themes/casper/default.hbs +2 -2
  14. package/content/themes/casper/package.json +3 -2
  15. package/content/themes/casper/post.hbs +1 -1
  16. package/content/themes/casper/yarn.lock +173 -123
  17. package/core/app.js +12 -1
  18. package/core/boot.js +47 -28
  19. package/core/bridge.js +10 -10
  20. package/core/built/assets/{chunk.3.324fd0cc598c73650219.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
  21. package/core/built/assets/ghost-dark-d690e732e17ffc794e2e59c1467ca282.css +1 -0
  22. package/core/built/assets/ghost.min-043bb7480a0810109b130f13b2a4235e.css +1 -0
  23. package/core/built/assets/{ghost.min-7da921f6c6cac3fe10da1ba104575440.js → ghost.min-bc72f685c1c9adc9885925c1412435a5.js} +563 -605
  24. package/core/built/assets/icons/audio-upload.svg +8 -0
  25. package/core/built/assets/icons/powered-by-tenor.svg +35 -0
  26. package/core/built/assets/icons/tenor.svg +7 -0
  27. package/core/built/assets/{vendor.min-413f887176a041e6dbf88214ca9a7481.js → vendor.min-d1234c632a54502777c34e50752fa3fc.js} +4622 -3631
  28. package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -2
  29. package/core/frontend/apps/amp/lib/views/amp.hbs +112 -0
  30. package/core/frontend/apps/private-blogging/index.js +1 -1
  31. package/core/frontend/apps/private-blogging/lib/router.js +1 -1
  32. package/core/frontend/services/apps/index.js +1 -1
  33. package/core/frontend/services/apps/loader.js +3 -3
  34. package/core/frontend/services/card-assets/index.js +0 -12
  35. package/core/frontend/services/card-assets/service.js +29 -28
  36. package/core/frontend/services/helpers/handlebars.js +1 -1
  37. package/core/frontend/services/routing/CollectionRouter.js +4 -5
  38. package/core/frontend/services/routing/EmailRouter.js +1 -1
  39. package/core/frontend/services/routing/ParentRouter.js +0 -8
  40. package/core/frontend/services/routing/PreviewRouter.js +1 -1
  41. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  42. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
  43. package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
  44. package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
  45. package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
  46. package/core/frontend/services/routing/router-manager.js +7 -2
  47. package/core/frontend/services/rss/generate-feed.js +2 -1
  48. package/core/frontend/services/theme-engine/middleware/ensure-active-theme.js +34 -0
  49. package/core/frontend/services/theme-engine/middleware/index.js +6 -0
  50. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +116 -0
  51. package/core/frontend/services/theme-engine/middleware/update-local-template-data.js +9 -0
  52. package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +57 -0
  53. package/core/frontend/src/cards/css/bookmark.css +72 -47
  54. package/core/frontend/src/cards/css/button.css +4 -0
  55. package/core/frontend/src/cards/css/callout.css +40 -3
  56. package/core/frontend/src/cards/css/gallery.css +15 -10
  57. package/core/frontend/src/cards/css/nft.css +20 -11
  58. package/core/frontend/src/cards/css/toggle.css +58 -0
  59. package/core/frontend/src/cards/js/toggle.js +16 -0
  60. package/core/frontend/web/middleware/error-handler.js +93 -0
  61. package/core/frontend/web/middleware/handle-image-sizes.js +3 -6
  62. package/core/frontend/web/middleware/index.js +1 -0
  63. package/core/frontend/web/middleware/serve-public-file.js +39 -16
  64. package/core/frontend/web/site.js +11 -14
  65. package/core/server/adapters/scheduling/SchedulingDefault.js +2 -2
  66. package/core/server/adapters/storage/LocalStorageBase.js +2 -2
  67. package/core/server/api/canary/authentication.js +1 -1
  68. package/core/server/api/canary/db.js +2 -2
  69. package/core/server/api/canary/media.js +3 -2
  70. package/core/server/api/canary/oembed.js +16 -1
  71. package/core/server/api/canary/session.js +1 -1
  72. package/core/server/api/canary/slugs.js +1 -1
  73. package/core/server/api/canary/utils/permissions.js +2 -2
  74. package/core/server/api/canary/utils/serializers/output/config.js +2 -6
  75. package/core/server/api/v2/authentication.js +1 -1
  76. package/core/server/api/v2/db.js +2 -2
  77. package/core/server/api/v2/session.js +1 -1
  78. package/core/server/api/v2/slugs.js +1 -1
  79. package/core/server/api/v2/utils/permissions.js +2 -2
  80. package/core/server/api/v3/authentication.js +1 -1
  81. package/core/server/api/v3/db.js +2 -2
  82. package/core/server/api/v3/session.js +1 -1
  83. package/core/server/api/v3/slugs.js +1 -1
  84. package/core/server/api/v3/utils/permissions.js +2 -2
  85. package/core/server/data/db/connection.js +7 -0
  86. package/core/server/data/db/state-manager.js +4 -4
  87. package/core/server/data/exporter/export-filename.js +1 -1
  88. package/core/server/data/importer/handlers/json.js +1 -1
  89. package/core/server/data/importer/import-manager.js +1 -1
  90. package/core/server/data/importer/importers/data/base.js +1 -1
  91. package/core/server/data/importer/importers/data/data-importer.js +3 -3
  92. package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
  93. package/core/server/data/migrations/utils.js +2 -2
  94. package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
  95. package/core/server/data/migrations/versions/1.25/1-update-koenig-beta-html.js +1 -0
  96. package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
  97. package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
  98. package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
  99. package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
  100. package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
  101. package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
  102. package/core/server/data/migrations/versions/3.1/08-add-uuid-values-to-members.js +1 -0
  103. package/core/server/data/migrations/versions/3.22/02-settings-key-renames.js +2 -0
  104. package/core/server/data/migrations/versions/3.22/05-migrate-members-subscription-settings.js +3 -0
  105. package/core/server/data/migrations/versions/3.22/06-migrate-stripe-connect-settings.js +2 -0
  106. package/core/server/data/migrations/versions/3.23/01-migrate-bulk-email-settings.js +1 -0
  107. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -0
  108. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -0
  109. package/core/server/data/migrations/versions/3.38/04-populate-recipient-filter-column.js +2 -0
  110. package/core/server/data/migrations/versions/4.0/01-update-mobiledoc.js +2 -0
  111. package/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js +4 -0
  112. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -0
  113. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -0
  114. package/core/server/data/migrations/versions/4.0/18-transform-urls-absolute-to-transform-ready.js +5 -0
  115. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -0
  116. package/core/server/data/migrations/versions/4.0/23-regenerate-posts-html.js +1 -0
  117. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +2 -1
  118. package/core/server/data/migrations/versions/4.12/02-fix-member-statuses.js +1 -0
  119. package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +3 -0
  120. package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +1 -0
  121. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +2 -0
  122. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +59 -0
  123. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -0
  124. package/core/server/data/migrations/versions/4.4/01-restore-free-members-signup-setting-from-backup.js +1 -0
  125. package/core/server/data/migrations/versions/4.6/01-remove-comped-status.js +1 -0
  126. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -0
  127. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -0
  128. package/core/server/data/migrations/versions/4.9/06-add-comped-status.js +1 -0
  129. package/core/server/data/migrations/versions/4.9/07-update-comped-members-status-events.js +1 -0
  130. package/core/server/data/schema/commands.js +2 -2
  131. package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
  132. package/core/server/data/schema/fixtures/index.js +8 -2
  133. package/core/server/ghost-server.js +2 -2
  134. package/core/server/lib/image/image-size.js +2 -2
  135. package/core/server/models/base/listeners.js +2 -2
  136. package/core/server/models/member-email-change-event.js +2 -2
  137. package/core/server/models/member-login-event.js +2 -2
  138. package/core/server/models/member-paid-subscription-event.js +3 -3
  139. package/core/server/models/member-payment-event.js +3 -3
  140. package/core/server/models/member-product-event.js +6 -6
  141. package/core/server/models/member-status-event.js +5 -3
  142. package/core/server/models/member-subscribe-event.js +9 -3
  143. package/core/server/models/relations/authors.js +1 -1
  144. package/core/server/models/settings.js +1 -1
  145. package/core/server/services/auth/passwordreset.js +1 -1
  146. package/core/server/services/auth/setup.js +1 -1
  147. package/core/server/services/email-analytics/jobs/index.js +1 -1
  148. package/core/server/services/mega/mega.js +6 -4
  149. package/core/server/services/mega/post-email-serializer.js +5 -1
  150. package/core/server/services/mega/segment-parser.js +1 -2
  151. package/core/server/services/mega/template.js +52 -37
  152. package/core/server/services/members/api.js +22 -0
  153. package/core/server/services/members/config.js +1 -1
  154. package/core/server/services/members/emails/signup-paid.js +168 -0
  155. package/core/server/services/members/service.js +6 -2
  156. package/core/server/services/members/stripe-connect.js +4 -2
  157. package/core/server/services/nft-oembed.js +13 -22
  158. package/core/server/services/oembed.js +28 -24
  159. package/core/server/services/permissions/can-this.js +1 -1
  160. package/core/server/services/public-config/config.js +1 -1
  161. package/core/server/services/redirects/api.js +20 -25
  162. package/core/server/services/redirects/index.js +18 -10
  163. package/core/server/services/redirects/utils.js +14 -0
  164. package/core/server/services/redirects/validation.js +10 -0
  165. package/core/server/services/route-settings/default-settings-manager.js +1 -1
  166. package/core/server/services/route-settings/index.js +40 -17
  167. package/core/server/services/route-settings/route-settings.js +120 -115
  168. package/core/server/services/route-settings/settings-loader.js +18 -36
  169. package/core/server/services/route-settings/yaml-parser.js +1 -1
  170. package/core/server/services/slack.js +1 -1
  171. package/core/server/services/themes/activation-bridge.js +3 -3
  172. package/core/server/services/themes/storage.js +2 -2
  173. package/core/server/services/twitter-embed.js +80 -0
  174. package/core/server/services/url/LocalFileCache.js +75 -0
  175. package/core/server/services/url/Resources.js +8 -2
  176. package/core/server/services/url/UrlGenerator.js +23 -20
  177. package/core/server/services/url/UrlService.js +75 -63
  178. package/core/server/services/url/index.js +17 -3
  179. package/core/server/services/xmlrpc.js +2 -2
  180. package/core/server/web/admin/app.js +7 -10
  181. package/core/server/web/admin/controller.js +35 -12
  182. package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
  183. package/core/server/web/admin/views/default-prod.html +4 -4
  184. package/core/server/web/admin/views/default.html +4 -4
  185. package/core/server/web/api/app.js +1 -1
  186. package/core/server/web/api/canary/admin/app.js +3 -6
  187. package/core/server/web/api/canary/admin/middleware.js +7 -7
  188. package/core/server/web/api/canary/admin/routes.js +5 -5
  189. package/core/server/web/api/canary/content/app.js +3 -6
  190. package/core/server/web/api/canary/content/middleware.js +3 -3
  191. package/core/server/web/api/v2/admin/app.js +3 -6
  192. package/core/server/web/api/v2/admin/middleware.js +7 -7
  193. package/core/server/web/api/v2/admin/routes.js +5 -5
  194. package/core/server/web/api/v2/content/app.js +3 -6
  195. package/core/server/web/api/v2/content/middleware.js +3 -3
  196. package/core/server/web/api/v3/admin/app.js +3 -6
  197. package/core/server/web/api/v3/admin/middleware.js +7 -7
  198. package/core/server/web/api/v3/admin/routes.js +5 -5
  199. package/core/server/web/api/v3/content/app.js +3 -6
  200. package/core/server/web/api/v3/content/middleware.js +3 -3
  201. package/core/server/web/members/app.js +6 -9
  202. package/core/server/web/oauth/app.js +0 -4
  203. package/core/server/web/parent/app.js +17 -9
  204. package/core/server/web/parent/frontend.js +1 -1
  205. package/core/server/web/shared/index.js +2 -2
  206. package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
  207. package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
  208. package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
  209. package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
  210. package/core/server/web/shared/middleware/error-handler.js +224 -0
  211. package/core/server/web/shared/{middlewares → middleware}/index.js +0 -4
  212. package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
  213. package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
  214. package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
  215. package/core/shared/config/defaults.json +13 -1
  216. package/core/shared/config/helpers.js +42 -0
  217. package/core/shared/config/loader.js +1 -1
  218. package/core/shared/labs.js +9 -5
  219. package/core/shared/sentry.js +1 -1
  220. package/loggingrc.js +19 -20
  221. package/package.json +38 -37
  222. package/yarn.lock +1064 -892
  223. package/content/themes/casper/assets/js/gallery-card.js +0 -24
  224. package/core/built/assets/ghost-dark-39fb496d051565531062d7e047d1c0b1.css +0 -1
  225. package/core/built/assets/ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css +0 -1
  226. package/core/frontend/services/theme-engine/middleware.js +0 -209
  227. package/core/server/data/schema/fixtures/utils.js +0 -321
  228. package/core/server/web/parent/vhost-utils.js +0 -39
  229. package/core/server/web/shared/middlewares/error-handler.js +0 -329
  230. package/core/server/web/shared/middlewares/maintenance.js +0 -25
@@ -35,7 +35,7 @@ module.exports = function parseYaml(file) {
35
35
 
36
36
  return parsed;
37
37
  } catch (error) {
38
- if (errors.utils.isIgnitionError(error)) {
38
+ if (errors.utils.isGhostError(error)) {
39
39
  throw error;
40
40
  }
41
41
 
@@ -136,7 +136,7 @@ function ping(post) {
136
136
  'Content-type': 'application/json'
137
137
  }
138
138
  }).catch(function (err) {
139
- logging.error(new errors.GhostError({
139
+ logging.error(new errors.InternalServerError({
140
140
  err: err,
141
141
  context: tpl(messages.requestFailedError, {service: 'slack'}),
142
142
  help: tpl(messages.requestFailedHelp, {url: 'https://ghost.org/docs/'})
@@ -14,7 +14,7 @@ module.exports = {
14
14
  if (labs.isSet('customThemeSettings')) {
15
15
  await customThemeSettings.api.activateTheme(themeName, checkedTheme);
16
16
  }
17
- bridge.activateTheme(theme, checkedTheme);
17
+ await bridge.activateTheme(theme, checkedTheme);
18
18
  },
19
19
  activateFromAPI: async (themeName, theme, checkedTheme) => {
20
20
  debug('Activating theme (method B on API "activate")', themeName);
@@ -22,7 +22,7 @@ module.exports = {
22
22
  if (labs.isSet('customThemeSettings')) {
23
23
  await customThemeSettings.api.activateTheme(themeName, checkedTheme);
24
24
  }
25
- bridge.activateTheme(theme, checkedTheme);
25
+ await bridge.activateTheme(theme, checkedTheme);
26
26
  },
27
27
  activateFromAPIOverride: async (themeName, theme, checkedTheme) => {
28
28
  debug('Activating theme (method C on API "override")', themeName);
@@ -30,6 +30,6 @@ module.exports = {
30
30
  if (labs.isSet('customThemeSettings')) {
31
31
  await customThemeSettings.api.activateTheme(themeName, checkedTheme);
32
32
  }
33
- bridge.activateTheme(theme, checkedTheme);
33
+ await bridge.activateTheme(theme, checkedTheme);
34
34
  }
35
35
  };
@@ -112,7 +112,7 @@ module.exports = {
112
112
  if (checkedTheme) {
113
113
  fs.remove(checkedTheme.path)
114
114
  .catch((err) => {
115
- logging.error(new errors.GhostError({err: err}));
115
+ logging.error(new errors.InternalServerError({err: err}));
116
116
  });
117
117
  }
118
118
 
@@ -120,7 +120,7 @@ module.exports = {
120
120
  getStorage()
121
121
  .delete(backupName)
122
122
  .catch((err) => {
123
- logging.error(new errors.GhostError({err: err}));
123
+ logging.error(new errors.InternalServerError({err: err}));
124
124
  });
125
125
  }
126
126
  },
@@ -0,0 +1,80 @@
1
+ const {extract} = require('oembed-parser');
2
+
3
+ /**
4
+ * @typedef {import('./oembed').ICustomProvider} ICustomProvider
5
+ * @typedef {import('./oembed').IExternalRequest} IExternalRequest
6
+ */
7
+
8
+ const TWITTER_PATH_REGEX = /\/status\/(\d+)/;
9
+
10
+ /**
11
+ * @implements ICustomProvider
12
+ */
13
+ class TwitterOEmbedProvider {
14
+ /**
15
+ * @param {object} dependencies
16
+ */
17
+ constructor(dependencies) {
18
+ this.dependencies = dependencies;
19
+ }
20
+
21
+ /**
22
+ * @param {URL} url
23
+ * @returns {Promise<boolean>}
24
+ */
25
+ async canSupportRequest(url) {
26
+ return url.host === 'twitter.com' && TWITTER_PATH_REGEX.test(url.pathname);
27
+ }
28
+
29
+ /**
30
+ * @param {URL} url
31
+ * @param {IExternalRequest} externalRequest
32
+ *
33
+ * @returns {Promise<object>}
34
+ */
35
+ async getOEmbedData(url, externalRequest) {
36
+ const [match, tweetId] = url.pathname.match(TWITTER_PATH_REGEX);
37
+ if (!match) {
38
+ return null;
39
+ }
40
+
41
+ /** @type {object} */
42
+ const oembedData = await extract(url.href);
43
+
44
+ if (this.dependencies.config.bearerToken) {
45
+ const query = {
46
+ expansions: ['attachments.poll_ids', 'attachments.media_keys', 'author_id', 'entities.mentions.username', 'geo.place_id', 'in_reply_to_user_id', 'referenced_tweets.id', 'referenced_tweets.id.author_id'],
47
+ 'media.fields': ['duration_ms', 'height', 'media_key', 'preview_image_url', 'type', 'url', 'width', 'public_metrics', 'alt_text'],
48
+ 'place.fields': ['contained_within', 'country', 'country_code', 'full_name', 'geo', 'id', 'name', 'place_type'],
49
+ 'poll.fields': ['duration_minutes', 'end_datetime', 'id', 'options', 'voting_status'],
50
+ 'tweet.fields': ['attachments', 'author_id', 'context_annotations', 'conversation_id', 'created_at', 'entities', 'geo', 'id', 'in_reply_to_user_id', 'lang', 'public_metrics', 'possibly_sensitive', 'referenced_tweets', 'reply_settings', 'source', 'text', 'withheld'],
51
+ 'user.fields': ['created_at', 'description', 'entities', 'id', 'location', 'name', 'pinned_tweet_id', 'profile_image_url', 'protected', 'public_metrics', 'url', 'username', 'verified', 'withheld']
52
+ };
53
+
54
+ const queryString = Object.keys(query).map((key) => {
55
+ return `${key}=${query[key].join(',')}`;
56
+ }).join('&');
57
+
58
+ try {
59
+ const result = await externalRequest(`https://api.twitter.com/2/tweets/${tweetId}?${queryString}`, {
60
+ responseType: 'json',
61
+ headers: {
62
+ Authorization: `Bearer ${this.dependencies.config.bearerToken}`
63
+ }
64
+ });
65
+
66
+ const body = JSON.parse(result.body);
67
+
68
+ oembedData.tweet_data = body.data;
69
+ } catch (err) {
70
+ this.dependencies.logging.error(err);
71
+ }
72
+ }
73
+
74
+ oembedData.type = 'twitter';
75
+
76
+ return oembedData;
77
+ }
78
+ }
79
+
80
+ module.exports = TwitterOEmbedProvider;
@@ -0,0 +1,75 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ class LocalFileCache {
5
+ /**
6
+ * @param {Object} options
7
+ * @param {String} options.storagePath - cached storage path
8
+ * @param {Boolean} options.writeDisabled - controls if cache can write
9
+ */
10
+ constructor({storagePath, writeDisabled}) {
11
+ const urlsStoragePath = path.join(storagePath, 'urls.json');
12
+ const resourcesCachePath = path.join(storagePath, 'resources.json');
13
+
14
+ this.storagePaths = {
15
+ urls: urlsStoragePath,
16
+ resources: resourcesCachePath
17
+ };
18
+ this.writeDisabled = writeDisabled;
19
+ }
20
+
21
+ /**
22
+ * Handles reading and parsing JSON from the filesystem.
23
+ * In case the file is corrupted or does not exist, returns null.
24
+ * @param {String} filePath path to read from
25
+ * @returns {Promise<Object>}
26
+ * @private
27
+ */
28
+ async readCacheFile(filePath) {
29
+ let cacheExists = false;
30
+ let cacheData = null;
31
+
32
+ try {
33
+ await fs.stat(filePath);
34
+ cacheExists = true;
35
+ } catch (e) {
36
+ cacheExists = false;
37
+ }
38
+
39
+ if (cacheExists) {
40
+ try {
41
+ const cacheFile = await fs.readFile(filePath, 'utf8');
42
+ cacheData = JSON.parse(cacheFile);
43
+ } catch (e) {
44
+ //noop as we'd start a long boot process if there are any errors in the file
45
+ }
46
+ }
47
+
48
+ return cacheData;
49
+ }
50
+
51
+ /**
52
+ *
53
+ * @param {'urls'|'resources'} type
54
+ * @returns {Promise<Object>}
55
+ */
56
+ async read(type) {
57
+ return await this.readCacheFile(this.storagePaths[type]);
58
+ }
59
+
60
+ /**
61
+ *
62
+ * @param {'urls'|'resources'} type of data to persist
63
+ * @param {Object} data - data to be persisted
64
+ * @returns {Promise<Object>}
65
+ */
66
+ async write(type, data) {
67
+ if (this.writeDisabled) {
68
+ return null;
69
+ }
70
+
71
+ return fs.writeFile(this.storagePaths[type], JSON.stringify(data, null, 4));
72
+ }
73
+ }
74
+
75
+ module.exports = LocalFileCache;
@@ -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
  }
@@ -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;
@@ -1,4 +1,3 @@
1
- const fs = require('fs-extra');
2
1
  const _debug = require('@tryghost/debug')._base;
3
2
  const debug = _debug('ghost:services:url:service');
4
3
  const _ = require('lodash');
@@ -22,18 +21,25 @@ class UrlService {
22
21
  /**
23
22
  *
24
23
  * @param {Object} options
25
- * @param {String} [options.urlCachePath] - path to store cached URLs at
24
+ * @param {Object} [options.cache] - cache handler instance
25
+ * @param {Function} [options.cache.read] - read cache by type
26
+ * @param {Function} [options.cache.write] - write into cache by type
26
27
  */
27
- constructor({urlCachePath} = {}) {
28
+ constructor({cache} = {}) {
28
29
  this.utils = urlUtils;
29
- this.urlCachePath = urlCachePath;
30
+ this.cache = cache;
31
+ this.onFinished = null;
30
32
  this.finished = false;
31
33
  this.urlGenerators = [];
32
34
 
33
35
  // Get urls
34
- this.urls = new Urls();
35
36
  this.queue = new Queue();
36
- this.resources = new Resources(this.queue);
37
+ // NOTE: Urls and Resources should not be initialized here but only in the init method.
38
+ // Way too many tests fail if the initialization is removed so leaving it as is for time being
39
+ this.urls = new Urls();
40
+ this.resources = new Resources({
41
+ queue: this.queue
42
+ });
37
43
 
38
44
  this._listeners();
39
45
  }
@@ -76,26 +82,41 @@ class UrlService {
76
82
  _onQueueEnded(event) {
77
83
  if (event === 'init') {
78
84
  this.finished = true;
85
+ if (this.onFinished) {
86
+ this.onFinished();
87
+ }
79
88
  }
80
89
  }
81
90
 
82
91
  /**
83
92
  * @description Router was created, connect it with a url generator.
84
- * @param {ExpressRouter} router
93
+ * @param {String} identifier frontend router ID reference
94
+ * @param {String} filter NQL filter
95
+ * @param {String} resourceType
96
+ * @param {String} permalink
85
97
  */
86
- onRouterAddedType(router) {
87
- debug('Registering route: ', router.name);
88
-
89
- let urlGenerator = new UrlGenerator(router, this.queue, this.resources, this.urls, this.urlGenerators.length);
98
+ onRouterAddedType(identifier, filter, resourceType, permalink) {
99
+ debug('Registering route: ', filter, resourceType, permalink);
100
+
101
+ let urlGenerator = new UrlGenerator({
102
+ identifier,
103
+ filter,
104
+ resourceType,
105
+ permalink,
106
+ queue: this.queue,
107
+ resources: this.resources,
108
+ urls: this.urls,
109
+ position: this.urlGenerators.length
110
+ });
90
111
  this.urlGenerators.push(urlGenerator);
91
112
  }
92
113
 
93
114
  /**
94
115
  * @description Router update handler - regenerates it's resources
95
- * @param {ExpressRouter} router
116
+ * @param {String} identifier router ID linked to the UrlGenerator
96
117
  */
97
- onRouterUpdated(router) {
98
- const generator = this.urlGenerators.find(g => g.router.id === router.id);
118
+ onRouterUpdated(identifier) {
119
+ const generator = this.urlGenerators.find(g => g.identifier === identifier);
99
120
  generator.regenerateResources();
100
121
  }
101
122
 
@@ -254,7 +275,7 @@ class UrlService {
254
275
  let urlGenerator;
255
276
 
256
277
  this.urlGenerators.every((_urlGenerator) => {
257
- if (_urlGenerator.router.identifier === routerId) {
278
+ if (_urlGenerator.identifier === routerId) {
258
279
  urlGenerator = _urlGenerator;
259
280
  return false;
260
281
  }
@@ -284,68 +305,59 @@ class UrlService {
284
305
  return null;
285
306
  }
286
307
 
287
- return _.find(this.urlGenerators, {uid: object.generatorId}).router.getPermalinks()
288
- .getValue(options);
289
- }
308
+ const permalink = _.find(this.urlGenerators, {uid: object.generatorId}).permalink;
290
309
 
291
- /**
292
- * @description Initializes components needed for the URL Service to function
293
- */
294
- async init() {
295
- this.resources.initResourceConfig();
296
- this.resources.initEvenListeners();
297
-
298
- const persistedUrls = await this.fetchUrls();
299
- if (persistedUrls) {
300
- this.urls = new Urls({
301
- urls: persistedUrls
302
- });
303
- this.finished = true;
304
- } else {
305
- await this.resources.fetchResources();
310
+ if (options.withUrlOptions) {
311
+ return urlUtils.urlJoin(permalink, '/:options(edit)?/');
306
312
  }
307
313
 
308
- // CASE: all resources are fetched, start the queue
309
- this.queue.start({
310
- event: 'init',
311
- tolerance: 100,
312
- requiredSubscriberCount: 1
313
- });
314
+ return permalink;
314
315
  }
315
316
 
316
- async persistUrls() {
317
- if (!labs.isSet('urlCache') || !this.urlCachePath) {
318
- return null;
319
- }
317
+ /**
318
+ * @description Initializes components needed for the URL Service to function
319
+ * @param {Object} options
320
+ * @param {Function} [options.onFinished] - callback when url generation is finished
321
+ * @param {Boolean} [options.urlCache] - whether to init using url cache or not
322
+ */
323
+ async init({onFinished, urlCache} = {}) {
324
+ this.onFinished = onFinished;
320
325
 
321
- return fs.writeFile(this.urlCachePath, JSON.stringify(this.urls.urls, null, 4));
322
- }
326
+ let persistedUrls;
327
+ let persistedResources;
323
328
 
324
- async fetchUrls() {
325
- if (!labs.isSet('urlCache') || !this.urlCachePath) {
326
- return null;
329
+ if (this.cache && (labs.isSet('urlCache') || urlCache)) {
330
+ persistedUrls = await this.cache.read('urls');
331
+ persistedResources = await this.cache.read('resources');
327
332
  }
328
333
 
329
- let urlsCacheExists = false;
330
- let urls;
334
+ if (persistedUrls && persistedResources) {
335
+ this.urls.urls = persistedUrls;
336
+ this.resources.data = persistedResources;
337
+ this.resources.initResourceConfig();
338
+ this.resources.initEvenListeners();
331
339
 
332
- try {
333
- await fs.stat(this.urlCachePath);
334
- urlsCacheExists = true;
335
- } catch (e) {
336
- urlsCacheExists = false;
340
+ this._onQueueEnded('init');
341
+ } else {
342
+ this.resources.initResourceConfig();
343
+ this.resources.initEvenListeners();
344
+ await this.resources.fetchResources();
345
+ // CASE: all resources are fetched, start the queue
346
+ this.queue.start({
347
+ event: 'init',
348
+ tolerance: 100,
349
+ requiredSubscriberCount: 1
350
+ });
337
351
  }
352
+ }
338
353
 
339
- if (urlsCacheExists) {
340
- try {
341
- const urlsFile = await fs.readFile(this.urlCachePath, 'utf8');
342
- urls = JSON.parse(urlsFile);
343
- } catch (e) {
344
- //noop as we'd start a long boot process if there are any errors in the file
345
- }
354
+ async shutdown() {
355
+ if (!labs.isSet('urlCache')) {
356
+ return null;
346
357
  }
347
358
 
348
- return urls;
359
+ await this.cache.write('urls', this.urls.urls);
360
+ await this.cache.write('resources', this.resources.getAll());
349
361
  }
350
362
 
351
363
  /**
@@ -1,12 +1,26 @@
1
- const path = require('path');
2
1
  const config = require('../../../shared/config');
2
+ const LocalFileCache = require('./LocalFileCache');
3
3
  const UrlService = require('./UrlService');
4
4
 
5
5
  // NOTE: instead of a path we could give UrlService a "data-resolver" of some sort
6
6
  // so it doesn't have to contain the logic to read data at all. This would be
7
7
  // a possible improvement in the future
8
- const urlCachePath = path.join(config.getContentPath('data'), 'urls.json');
9
- const urlService = new UrlService({urlCachePath});
8
+ let writeDisabled = false;
9
+ let storagePath = config.getContentPath('data');
10
+
11
+ // TODO: remove this hack in favor of loading from the content path when it's possible to do so
12
+ // by mocking content folders in pre-boot phase
13
+ if (process.env.NODE_ENV.startsWith('test')){
14
+ storagePath = config.get('paths').urlCache;
15
+
16
+ // NOTE: prevents test suites from overwriting cache fixtures.
17
+ // A better solution would be injecting a different implementation of the
18
+ // cache based on the environment, this approach should do the trick for now
19
+ writeDisabled = true;
20
+ }
21
+
22
+ const cache = new LocalFileCache({storagePath, writeDisabled});
23
+ const urlService = new UrlService({cache});
10
24
 
11
25
  // Singleton
12
26
  module.exports = urlService;
@@ -87,7 +87,7 @@ function ping(post) {
87
87
  if (!goodResponse.test(res.body)) {
88
88
  const matches = res.body.match(errorMessage);
89
89
  const message = matches ? matches[1] : res.body;
90
- throw new errors.GhostError({message});
90
+ throw new errors.InternalServerError({message});
91
91
  }
92
92
  })
93
93
  .catch(function (err) {
@@ -100,7 +100,7 @@ function ping(post) {
100
100
  help: tpl(messages.requestFailedHelp, {url: 'https://ghost.org/docs/'})
101
101
  });
102
102
  } else {
103
- error = new errors.GhostError({
103
+ error = new errors.InternalServerError({
104
104
  err: err,
105
105
  message: err.message,
106
106
  context: tpl(messages.requestFailedError, {service: 'xmlrpc'}),
@@ -5,7 +5,7 @@ const config = require('../../../shared/config');
5
5
  const constants = require('@tryghost/constants');
6
6
  const urlUtils = require('../../../shared/url-utils');
7
7
  const shared = require('../shared');
8
- const adminMiddleware = require('./middleware');
8
+ const redirectAdminUrls = require('./middleware/redirect-admin-urls');
9
9
 
10
10
  module.exports = function setupAdminApp() {
11
11
  debug('Admin setup start');
@@ -26,29 +26,26 @@ module.exports = function setupAdminApp() {
26
26
  });
27
27
  }
28
28
 
29
- // Render error page in case of maintenance
30
- adminApp.use(shared.middlewares.maintenance);
31
-
32
29
  // Force SSL if required
33
30
  // must happen AFTER asset loading and BEFORE routing
34
- adminApp.use(shared.middlewares.urlRedirects.adminSSLAndHostRedirect);
31
+ adminApp.use(shared.middleware.urlRedirects.adminSSLAndHostRedirect);
35
32
 
36
33
  // Add in all trailing slashes & remove uppercase
37
34
  // must happen AFTER asset loading and BEFORE routing
38
- adminApp.use(shared.middlewares.prettyUrls);
35
+ adminApp.use(shared.middleware.prettyUrls);
39
36
 
40
37
  // Cache headers go last before serving the request
41
38
  // Admin is currently set to not be cached at all
42
- adminApp.use(shared.middlewares.cacheControl('private'));
39
+ adminApp.use(shared.middleware.cacheControl('private'));
43
40
 
44
41
  // Special redirects for the admin (these should have their own cache-control headers)
45
- adminApp.use(adminMiddleware);
42
+ adminApp.use(redirectAdminUrls);
46
43
 
47
44
  // Finally, routing
48
45
  adminApp.get('*', require('./controller'));
49
46
 
50
- adminApp.use(shared.middlewares.errorHandler.pageNotFound);
51
- adminApp.use(shared.middlewares.errorHandler.handleHTMLResponse);
47
+ adminApp.use(shared.middleware.errorHandler.pageNotFound);
48
+ adminApp.use(shared.middleware.errorHandler.handleHTMLResponse);
52
49
 
53
50
  debug('Admin setup end');
54
51