ghost 4.22.2 → 4.24.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 (118) hide show
  1. package/.c8rc.json +24 -0
  2. package/Gruntfile.js +0 -1
  3. package/content/public/README.md +3 -0
  4. package/core/app.js +12 -1
  5. package/core/boot.js +45 -26
  6. package/core/bridge.js +10 -10
  7. package/core/built/assets/{chunk.3.324fd0cc598c73650219.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
  8. package/core/built/assets/{ghost-dark-39fb496d051565531062d7e047d1c0b1.css → ghost-dark-e7b57ab951512c5719aee89b16b9a448.css} +1 -1
  9. package/core/built/assets/{ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css → ghost.min-7f3603dbeb5ebf0ec09e207ae82fb4e3.css} +1 -1
  10. package/core/built/assets/{ghost.min-7da921f6c6cac3fe10da1ba104575440.js → ghost.min-d5595f9c71ebc534ccf9ac78483d357c.js} +138 -105
  11. package/core/built/assets/icons/powered-by-tenor.svg +35 -0
  12. package/core/built/assets/icons/tenor.svg +7 -0
  13. package/core/built/assets/{vendor.min-413f887176a041e6dbf88214ca9a7481.js → vendor.min-1a84ac3ef74edf31c6e86810b45221cc.js} +2964 -2434
  14. package/core/frontend/apps/amp/lib/views/amp.hbs +104 -0
  15. package/core/frontend/apps/private-blogging/lib/router.js +1 -1
  16. package/core/frontend/services/card-assets/index.js +0 -12
  17. package/core/frontend/services/card-assets/service.js +35 -26
  18. package/core/frontend/services/routing/CollectionRouter.js +4 -5
  19. package/core/frontend/services/routing/EmailRouter.js +1 -1
  20. package/core/frontend/services/routing/ParentRouter.js +0 -8
  21. package/core/frontend/services/routing/PreviewRouter.js +1 -1
  22. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  23. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
  24. package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
  25. package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
  26. package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
  27. package/core/frontend/services/routing/router-manager.js +7 -2
  28. package/core/frontend/services/rss/generate-feed.js +2 -1
  29. package/core/frontend/src/cards/css/bookmark.css +72 -47
  30. package/core/frontend/src/cards/css/callout.css +41 -4
  31. package/core/frontend/src/cards/css/gallery.css +15 -10
  32. package/core/frontend/src/cards/css/nft.css +20 -11
  33. package/core/frontend/src/cards/css/toggle.css +58 -0
  34. package/core/frontend/src/cards/js/toggle.js +16 -0
  35. package/core/frontend/web/middleware/serve-public-file.js +39 -16
  36. package/core/frontend/web/site.js +11 -14
  37. package/core/server/api/canary/authentication.js +1 -1
  38. package/core/server/api/canary/utils/serializers/output/config.js +1 -1
  39. package/core/server/api/v2/authentication.js +1 -1
  40. package/core/server/api/v3/authentication.js +1 -1
  41. package/core/server/data/db/connection.js +7 -0
  42. package/core/server/data/importer/importers/data/data-importer.js +3 -3
  43. package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
  44. package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
  45. package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
  46. package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
  47. package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
  48. package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
  49. package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
  50. package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
  51. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +58 -0
  52. package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
  53. package/core/server/data/schema/fixtures/index.js +8 -2
  54. package/core/server/services/email-analytics/jobs/index.js +1 -1
  55. package/core/server/services/mega/post-email-serializer.js +5 -1
  56. package/core/server/services/mega/segment-parser.js +1 -2
  57. package/core/server/services/mega/template.js +52 -37
  58. package/core/server/services/nft-oembed.js +7 -21
  59. package/core/server/services/oembed.js +24 -24
  60. package/core/server/services/public-config/config.js +1 -1
  61. package/core/server/services/redirects/api.js +18 -23
  62. package/core/server/services/redirects/index.js +18 -10
  63. package/core/server/services/redirects/utils.js +14 -0
  64. package/core/server/services/redirects/validation.js +10 -0
  65. package/core/server/services/route-settings/index.js +40 -17
  66. package/core/server/services/route-settings/route-settings.js +127 -114
  67. package/core/server/services/route-settings/settings-loader.js +14 -32
  68. package/core/server/services/themes/activation-bridge.js +3 -3
  69. package/core/server/services/url/LocalFileCache.js +75 -0
  70. package/core/server/services/url/Resources.js +8 -2
  71. package/core/server/services/url/UrlGenerator.js +23 -20
  72. package/core/server/services/url/UrlService.js +75 -63
  73. package/core/server/services/url/index.js +17 -3
  74. package/core/server/web/admin/app.js +7 -10
  75. package/core/server/web/admin/controller.js +35 -12
  76. package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
  77. package/core/server/web/admin/views/default-prod.html +4 -4
  78. package/core/server/web/admin/views/default.html +4 -4
  79. package/core/server/web/api/app.js +1 -1
  80. package/core/server/web/api/canary/admin/app.js +3 -6
  81. package/core/server/web/api/canary/admin/middleware.js +6 -6
  82. package/core/server/web/api/canary/admin/routes.js +5 -5
  83. package/core/server/web/api/canary/content/app.js +3 -6
  84. package/core/server/web/api/canary/content/middleware.js +3 -3
  85. package/core/server/web/api/v2/admin/app.js +3 -6
  86. package/core/server/web/api/v2/admin/middleware.js +6 -6
  87. package/core/server/web/api/v2/admin/routes.js +5 -5
  88. package/core/server/web/api/v2/content/app.js +3 -6
  89. package/core/server/web/api/v2/content/middleware.js +3 -3
  90. package/core/server/web/api/v3/admin/app.js +3 -6
  91. package/core/server/web/api/v3/admin/middleware.js +6 -6
  92. package/core/server/web/api/v3/admin/routes.js +5 -5
  93. package/core/server/web/api/v3/content/app.js +3 -6
  94. package/core/server/web/api/v3/content/middleware.js +3 -3
  95. package/core/server/web/members/app.js +6 -9
  96. package/core/server/web/oauth/app.js +0 -4
  97. package/core/server/web/parent/app.js +17 -9
  98. package/core/server/web/parent/frontend.js +1 -1
  99. package/core/server/web/shared/index.js +2 -2
  100. package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
  101. package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
  102. package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
  103. package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
  104. package/core/server/web/shared/{middlewares → middleware}/error-handler.js +70 -53
  105. package/core/server/web/shared/{middlewares → middleware}/index.js +0 -4
  106. package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
  107. package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
  108. package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
  109. package/core/shared/config/defaults.json +7 -1
  110. package/core/shared/config/helpers.js +42 -0
  111. package/core/shared/config/loader.js +1 -1
  112. package/core/shared/labs.js +7 -2
  113. package/loggingrc.js +19 -20
  114. package/package.json +35 -34
  115. package/yarn.lock +822 -345
  116. package/core/server/data/schema/fixtures/utils.js +0 -321
  117. package/core/server/web/parent/vhost-utils.js +0 -39
  118. package/core/server/web/shared/middlewares/maintenance.js +0 -25
@@ -1,9 +1,7 @@
1
1
  const fs = require('fs-extra');
2
- const path = require('path');
3
2
  const debug = require('@tryghost/debug')('frontend:services:settings:settings-loader');
4
3
  const tpl = require('@tryghost/tpl');
5
4
  const errors = require('@tryghost/errors');
6
- const config = require('../../../shared/config');
7
5
  const validate = require('./validate');
8
6
 
9
7
  const messages = {
@@ -14,23 +12,12 @@ class SettingsLoader {
14
12
  /**
15
13
  * @param {Object} options
16
14
  * @param {Function} options.parseYaml yaml parser
15
+ * @param {String} options.settingFilePath routes settings file path
17
16
  */
18
- constructor({parseYaml}) {
17
+ constructor({parseYaml, settingFilePath}) {
19
18
  this.parseYaml = parseYaml;
20
- }
21
-
22
- /**
23
- * NOTE: this method will have to go to an external module to reuse in redirects settings
24
- * @param {String} setting type of the settings to load, e.g:'routes' or 'redirects'
25
- * @returns {String} setting file path
26
- */
27
- getSettingFilePath(setting) {
28
- // we only support the `yaml` file extension. `yml` will be ignored.
29
- const fileName = `${setting}.yaml`;
30
- const contentPath = config.getContentPath('settings');
31
- const filePath = path.join(contentPath, fileName);
32
19
 
33
- return filePath;
20
+ this.settingFilePath = settingFilePath;
34
21
  }
35
22
 
36
23
  /**
@@ -40,16 +27,12 @@ class SettingsLoader {
40
27
  * @returns {Promise<Object>} settingsFile
41
28
  */
42
29
  async loadSettings() {
43
- const setting = 'routes';
44
- const filePath = this.getSettingFilePath(setting);
45
-
46
30
  try {
47
- const file = await fs.readFile(filePath, 'utf8');
48
- debug('settings file found for', setting);
31
+ const file = await fs.readFile(this.settingFilePath, 'utf8');
32
+ debug('routes settings file found for:', this.settingFilePath);
49
33
 
50
34
  const object = this.parseYaml(file);
51
-
52
- debug('YAML settings file parsed:', filePath);
35
+ debug('YAML settings file parsed:', this.settingFilePath);
53
36
 
54
37
  return validate(object);
55
38
  } catch (err) {
@@ -59,8 +42,8 @@ class SettingsLoader {
59
42
 
60
43
  throw new errors.GhostError({
61
44
  message: tpl(messages.settingsLoaderError, {
62
- setting: setting,
63
- path: filePath
45
+ setting: 'routes',
46
+ path: this.settingFilePath
64
47
  }),
65
48
  err: err
66
49
  });
@@ -74,14 +57,13 @@ class SettingsLoader {
74
57
  * @returns {Object} settingsFile in following format: {routes: {}, collections: {}, resources: {}}
75
58
  */
76
59
  loadSettingsSync() {
77
- const setting = 'routes';
78
- const filePath = this.getSettingFilePath(setting);
79
-
80
60
  try {
81
- const file = fs.readFileSync(filePath, 'utf8');
82
- debug('settings file found for', setting);
61
+ const file = fs.readFileSync(this.settingFilePath, 'utf8');
62
+ debug('routes settings file found for:', this.settingFilePath);
83
63
 
84
64
  const object = this.parseYaml(file);
65
+ debug('YAML settings file parsed:', this.settingFilePath);
66
+
85
67
  return validate(object);
86
68
  } catch (err) {
87
69
  if (errors.utils.isIgnitionError(err)) {
@@ -90,8 +72,8 @@ class SettingsLoader {
90
72
 
91
73
  throw new errors.GhostError({
92
74
  message: tpl(messages.settingsLoaderError, {
93
- setting: setting,
94
- path: filePath
75
+ setting: 'routes',
76
+ path: this.settingFilePath
95
77
  }),
96
78
  err: err
97
79
  });
@@ -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
  };
@@ -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;
@@ -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
 
@@ -1,10 +1,20 @@
1
1
  const debug = require('@tryghost/debug')('web:admin:controller');
2
+ const errors = require('@tryghost/errors');
3
+ const tpl = require('@tryghost/tpl');
2
4
  const path = require('path');
3
5
  const fs = require('fs');
4
6
  const crypto = require('crypto');
5
7
  const config = require('../../../shared/config');
6
8
  const updateCheck = require('../../update-check');
7
9
 
10
+ const messages = {
11
+ templateError: {
12
+ message: 'Unable to find admin template file {templatePath}',
13
+ context: 'These template files are generated as part of the build process',
14
+ help: 'Please see {link}'
15
+ }
16
+ };
17
+
8
18
  /**
9
19
  * @description Admin controller to handle /ghost/ requests.
10
20
  *
@@ -23,18 +33,31 @@ module.exports = function adminController(req, res) {
23
33
  const templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate);
24
34
  const headers = {};
25
35
 
26
- // Generate our own ETag header
27
- // `sendFile` by default uses filesize+lastmod date to generate an etag.
28
- // That doesn't work for admin templates because the filesize doesn't change between versions
29
- // and `npm pack` sets a fixed lastmod date for every file meaning the default etag never changes
30
- const fileBuffer = fs.readFileSync(templatePath);
31
- const hashSum = crypto.createHash('md5');
32
- hashSum.update(fileBuffer);
33
- headers.ETag = hashSum.digest('hex');
36
+ try {
37
+ // Generate our own ETag header
38
+ // `sendFile` by default uses filesize+lastmod date to generate an etag.
39
+ // That doesn't work for admin templates because the filesize doesn't change between versions
40
+ // and `npm pack` sets a fixed lastmod date for every file meaning the default etag never changes
41
+ const fileBuffer = fs.readFileSync(templatePath);
42
+ const hashSum = crypto.createHash('md5');
43
+ hashSum.update(fileBuffer);
44
+ headers.ETag = hashSum.digest('hex');
34
45
 
35
- if (config.get('adminFrameProtection')) {
36
- headers['X-Frame-Options'] = 'sameorigin';
37
- }
46
+ if (config.get('adminFrameProtection')) {
47
+ headers['X-Frame-Options'] = 'sameorigin';
48
+ }
38
49
 
39
- res.sendFile(templatePath, {headers});
50
+ res.sendFile(templatePath, {headers});
51
+ } catch (error) {
52
+ if (error.code === 'ENOENT') {
53
+ throw new errors.IncorrectUsageError({
54
+ message: tpl(messages.templateError.message, {templatePath}),
55
+ context: tpl(messages.templateError.context),
56
+ help: tpl(messages.templateError.help, {link: 'https://ghost.org/docs/install/source/'}),
57
+ error: error
58
+ });
59
+ } else {
60
+ throw error;
61
+ }
62
+ }
40
63
  };
@@ -0,0 +1,15 @@
1
+ const urlUtils = require('../../../../shared/url-utils');
2
+
3
+ function redirectAdminUrls(req, res, next) {
4
+ const subdir = urlUtils.getSubdir();
5
+ const ghostPathRegex = new RegExp(`^${subdir}/ghost/(.+)`);
6
+ const ghostPathMatch = req.originalUrl.match(ghostPathRegex);
7
+
8
+ if (ghostPathMatch) {
9
+ return res.redirect(urlUtils.urlJoin(urlUtils.urlFor('admin'), '#', ghostPathMatch[1]));
10
+ }
11
+
12
+ next();
13
+ }
14
+
15
+ module.exports = redirectAdminUrls;