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.
- package/.c8rc.json +24 -0
- package/Gruntfile.js +0 -1
- package/content/public/README.md +3 -0
- package/core/app.js +12 -1
- package/core/boot.js +45 -26
- package/core/bridge.js +10 -10
- package/core/built/assets/{chunk.3.324fd0cc598c73650219.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
- package/core/built/assets/{ghost-dark-39fb496d051565531062d7e047d1c0b1.css → ghost-dark-e7b57ab951512c5719aee89b16b9a448.css} +1 -1
- package/core/built/assets/{ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css → ghost.min-7f3603dbeb5ebf0ec09e207ae82fb4e3.css} +1 -1
- package/core/built/assets/{ghost.min-7da921f6c6cac3fe10da1ba104575440.js → ghost.min-d5595f9c71ebc534ccf9ac78483d357c.js} +138 -105
- package/core/built/assets/icons/powered-by-tenor.svg +35 -0
- package/core/built/assets/icons/tenor.svg +7 -0
- package/core/built/assets/{vendor.min-413f887176a041e6dbf88214ca9a7481.js → vendor.min-1a84ac3ef74edf31c6e86810b45221cc.js} +2964 -2434
- package/core/frontend/apps/amp/lib/views/amp.hbs +104 -0
- package/core/frontend/apps/private-blogging/lib/router.js +1 -1
- package/core/frontend/services/card-assets/index.js +0 -12
- package/core/frontend/services/card-assets/service.js +35 -26
- package/core/frontend/services/routing/CollectionRouter.js +4 -5
- package/core/frontend/services/routing/EmailRouter.js +1 -1
- package/core/frontend/services/routing/ParentRouter.js +0 -8
- package/core/frontend/services/routing/PreviewRouter.js +1 -1
- package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
- package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
- package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
- package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
- package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
- package/core/frontend/services/routing/router-manager.js +7 -2
- package/core/frontend/services/rss/generate-feed.js +2 -1
- package/core/frontend/src/cards/css/bookmark.css +72 -47
- package/core/frontend/src/cards/css/callout.css +41 -4
- package/core/frontend/src/cards/css/gallery.css +15 -10
- package/core/frontend/src/cards/css/nft.css +20 -11
- package/core/frontend/src/cards/css/toggle.css +58 -0
- package/core/frontend/src/cards/js/toggle.js +16 -0
- package/core/frontend/web/middleware/serve-public-file.js +39 -16
- package/core/frontend/web/site.js +11 -14
- package/core/server/api/canary/authentication.js +1 -1
- package/core/server/api/canary/utils/serializers/output/config.js +1 -1
- package/core/server/api/v2/authentication.js +1 -1
- package/core/server/api/v3/authentication.js +1 -1
- package/core/server/data/db/connection.js +7 -0
- package/core/server/data/importer/importers/data/data-importer.js +3 -3
- package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
- package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
- package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
- package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
- package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
- package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
- package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
- package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
- package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +58 -0
- package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
- package/core/server/data/schema/fixtures/index.js +8 -2
- package/core/server/services/email-analytics/jobs/index.js +1 -1
- package/core/server/services/mega/post-email-serializer.js +5 -1
- package/core/server/services/mega/segment-parser.js +1 -2
- package/core/server/services/mega/template.js +52 -37
- package/core/server/services/nft-oembed.js +7 -21
- package/core/server/services/oembed.js +24 -24
- package/core/server/services/public-config/config.js +1 -1
- package/core/server/services/redirects/api.js +18 -23
- package/core/server/services/redirects/index.js +18 -10
- package/core/server/services/redirects/utils.js +14 -0
- package/core/server/services/redirects/validation.js +10 -0
- package/core/server/services/route-settings/index.js +40 -17
- package/core/server/services/route-settings/route-settings.js +127 -114
- package/core/server/services/route-settings/settings-loader.js +14 -32
- package/core/server/services/themes/activation-bridge.js +3 -3
- package/core/server/services/url/LocalFileCache.js +75 -0
- package/core/server/services/url/Resources.js +8 -2
- package/core/server/services/url/UrlGenerator.js +23 -20
- package/core/server/services/url/UrlService.js +75 -63
- package/core/server/services/url/index.js +17 -3
- package/core/server/web/admin/app.js +7 -10
- package/core/server/web/admin/controller.js +35 -12
- package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +1 -1
- package/core/server/web/api/canary/admin/app.js +3 -6
- package/core/server/web/api/canary/admin/middleware.js +6 -6
- package/core/server/web/api/canary/admin/routes.js +5 -5
- package/core/server/web/api/canary/content/app.js +3 -6
- package/core/server/web/api/canary/content/middleware.js +3 -3
- package/core/server/web/api/v2/admin/app.js +3 -6
- package/core/server/web/api/v2/admin/middleware.js +6 -6
- package/core/server/web/api/v2/admin/routes.js +5 -5
- package/core/server/web/api/v2/content/app.js +3 -6
- package/core/server/web/api/v2/content/middleware.js +3 -3
- package/core/server/web/api/v3/admin/app.js +3 -6
- package/core/server/web/api/v3/admin/middleware.js +6 -6
- package/core/server/web/api/v3/admin/routes.js +5 -5
- package/core/server/web/api/v3/content/app.js +3 -6
- package/core/server/web/api/v3/content/middleware.js +3 -3
- package/core/server/web/members/app.js +6 -9
- package/core/server/web/oauth/app.js +0 -4
- package/core/server/web/parent/app.js +17 -9
- package/core/server/web/parent/frontend.js +1 -1
- package/core/server/web/shared/index.js +2 -2
- package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/error-handler.js +70 -53
- package/core/server/web/shared/{middlewares → middleware}/index.js +0 -4
- package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
- package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
- package/core/shared/config/defaults.json +7 -1
- package/core/shared/config/helpers.js +42 -0
- package/core/shared/config/loader.js +1 -1
- package/core/shared/labs.js +7 -2
- package/loggingrc.js +19 -20
- package/package.json +35 -34
- package/yarn.lock +822 -345
- package/core/server/data/schema/fixtures/utils.js +0 -321
- package/core/server/web/parent/vhost-utils.js +0 -39
- 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
|
-
|
|
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(
|
|
48
|
-
debug('settings file found for',
|
|
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:
|
|
63
|
-
path:
|
|
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(
|
|
82
|
-
debug('settings file found for',
|
|
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:
|
|
94
|
-
path:
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @param {Object} options
|
|
38
|
+
* @param {String} options.identifier frontend router ID reference
|
|
39
|
+
* @param {String} options.filter NQL filter string
|
|
40
|
+
* @param {String} options.resourceType resource type (e.g. 'posts', 'tags')
|
|
41
|
+
* @param {String} options.permalink permalink string
|
|
42
|
+
* @param {Object} options.queue instance of the backend Queue
|
|
43
|
+
* @param {Object} options.resources instance of the backend Resources
|
|
44
|
+
* @param {Object} options.urls instance of the backend URLs (used to store the urls)
|
|
45
|
+
* @param {Number} options.position an ID of the generator
|
|
46
|
+
*/
|
|
47
|
+
constructor({identifier, filter, resourceType, permalink, queue, resources, urls, position}) {
|
|
48
|
+
this.identifier = identifier;
|
|
49
|
+
this.resourceType = resourceType;
|
|
50
|
+
this.permalink = permalink;
|
|
38
51
|
this.queue = queue;
|
|
39
52
|
this.urls = urls;
|
|
40
53
|
this.resources = resources;
|
|
41
54
|
this.uid = position;
|
|
42
55
|
|
|
43
|
-
debug('constructor', this.toString());
|
|
44
|
-
|
|
45
56
|
// CASE: routers can define custom filters, but not required.
|
|
46
|
-
if (
|
|
47
|
-
this.filter =
|
|
57
|
+
if (filter) {
|
|
58
|
+
this.filter = filter;
|
|
48
59
|
this.nql = nql(this.filter, {
|
|
49
60
|
expansions: EXPANSIONS,
|
|
50
61
|
transformer: nql.utils.mapKeyValues({
|
|
@@ -110,10 +121,10 @@ class UrlGenerator {
|
|
|
110
121
|
* @private
|
|
111
122
|
*/
|
|
112
123
|
_onInit() {
|
|
113
|
-
debug('_onInit', this.
|
|
124
|
+
debug('_onInit', this.resourceType);
|
|
114
125
|
|
|
115
126
|
// @NOTE: get the resources of my type e.g. posts.
|
|
116
|
-
const resources = this.resources.getAllByType(this.
|
|
127
|
+
const resources = this.resources.getAllByType(this.resourceType);
|
|
117
128
|
|
|
118
129
|
debug(resources.length);
|
|
119
130
|
|
|
@@ -131,7 +142,7 @@ class UrlGenerator {
|
|
|
131
142
|
debug('onAdded', this.toString());
|
|
132
143
|
|
|
133
144
|
// CASE: you are type "pages", but the incoming type is "users"
|
|
134
|
-
if (event.type !== this.
|
|
145
|
+
if (event.type !== this.resourceType) {
|
|
135
146
|
return;
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -182,8 +193,7 @@ class UrlGenerator {
|
|
|
182
193
|
* @NOTE We currently generate relative urls (https://github.com/TryGhost/Ghost/commit/7b0d5d465ba41073db0c3c72006da625fa11df32).
|
|
183
194
|
*/
|
|
184
195
|
_generateUrl(resource) {
|
|
185
|
-
|
|
186
|
-
return localUtils.replacePermalink(permalink, resource.data);
|
|
196
|
+
return localUtils.replacePermalink(this.permalink, resource.data);
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
/**
|
|
@@ -214,7 +224,7 @@ class UrlGenerator {
|
|
|
214
224
|
action: 'added:' + resource.data.id,
|
|
215
225
|
eventData: {
|
|
216
226
|
id: resource.data.id,
|
|
217
|
-
type: this.
|
|
227
|
+
type: this.resourceType
|
|
218
228
|
}
|
|
219
229
|
});
|
|
220
230
|
};
|
|
@@ -246,19 +256,12 @@ class UrlGenerator {
|
|
|
246
256
|
|
|
247
257
|
/**
|
|
248
258
|
* @description Get all urls of this url generator.
|
|
259
|
+
* NOTE: the method is only used for testing purposes at the moment.
|
|
249
260
|
* @returns {Array}
|
|
250
261
|
*/
|
|
251
262
|
getUrls() {
|
|
252
263
|
return this.urls.getByGeneratorId(this.uid);
|
|
253
264
|
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* @description Override of `toString`
|
|
257
|
-
* @returns {string}
|
|
258
|
-
*/
|
|
259
|
-
toString() {
|
|
260
|
-
return this.router.toString();
|
|
261
|
-
}
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
module.exports = UrlGenerator;
|
|
@@ -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 {
|
|
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({
|
|
28
|
+
constructor({cache} = {}) {
|
|
28
29
|
this.utils = urlUtils;
|
|
29
|
-
this.
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
87
|
-
debug('Registering route: ',
|
|
88
|
-
|
|
89
|
-
let urlGenerator = new UrlGenerator(
|
|
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 {
|
|
116
|
+
* @param {String} identifier router ID linked to the UrlGenerator
|
|
96
117
|
*/
|
|
97
|
-
onRouterUpdated(
|
|
98
|
-
const generator = this.urlGenerators.find(g => g.
|
|
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.
|
|
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
|
-
|
|
288
|
-
.getValue(options);
|
|
289
|
-
}
|
|
308
|
+
const permalink = _.find(this.urlGenerators, {uid: object.generatorId}).permalink;
|
|
290
309
|
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
309
|
-
this.queue.start({
|
|
310
|
-
event: 'init',
|
|
311
|
-
tolerance: 100,
|
|
312
|
-
requiredSubscriberCount: 1
|
|
313
|
-
});
|
|
314
|
+
return permalink;
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
326
|
+
let persistedUrls;
|
|
327
|
+
let persistedResources;
|
|
323
328
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
42
|
+
adminApp.use(redirectAdminUrls);
|
|
46
43
|
|
|
47
44
|
// Finally, routing
|
|
48
45
|
adminApp.get('*', require('./controller'));
|
|
49
46
|
|
|
50
|
-
adminApp.use(shared.
|
|
51
|
-
adminApp.use(shared.
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
if (config.get('adminFrameProtection')) {
|
|
47
|
+
headers['X-Frame-Options'] = 'sameorigin';
|
|
48
|
+
}
|
|
38
49
|
|
|
39
|
-
|
|
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;
|