ghost 4.15.0 → 4.17.1
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/.eslintrc.js +7 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/default.hbs +2 -2
- package/content/themes/casper/package.json +1 -1
- package/content/themes/casper/page.hbs +28 -26
- package/content/themes/casper/partials/post-card.hbs +2 -2
- package/content/themes/casper/post.hbs +67 -65
- package/content/themes/casper/tag.hbs +2 -2
- package/core/boot.js +7 -7
- package/core/bridge.js +4 -3
- package/core/built/assets/{chunk.3.4b1d9e20e57164ac9c29.js → chunk.3.b80d3e1e6b8556aaff3c.js} +72 -71
- package/core/built/assets/ghost-dark-f7bf2dd8d8c702716f75bfa4ccd92df2.css +1 -0
- package/core/built/assets/{ghost.min-e35cfee26d942c364166f57f3dcc9e75.js → ghost.min-52a5420ffcea6bf17761b5c59cf020e2.js} +979 -908
- package/core/built/assets/ghost.min-741246f42f000c073999a5363434ea2c.css +1 -0
- package/core/built/assets/icons/discount-bubble.svg +1 -0
- package/core/built/assets/{vendor.min-ca33abc718f21a51327841d58f8875d0.js → vendor.min-1bfc9d56d27508db88ef417deb55f16f.js} +454 -434
- package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +2 -2
- package/core/frontend/apps/amp/lib/helpers/amp_components.js +2 -1
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +5 -1
- package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
- package/core/frontend/apps/amp/lib/router.js +8 -4
- package/core/frontend/apps/private-blogging/index.js +13 -5
- package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
- package/core/frontend/apps/private-blogging/lib/middleware.js +8 -3
- package/core/frontend/helpers/asset.js +10 -2
- package/core/frontend/helpers/author.js +5 -3
- package/core/frontend/helpers/authors.js +4 -3
- package/core/frontend/helpers/body_class.js +1 -1
- package/core/frontend/helpers/cancel_link.js +9 -2
- package/core/frontend/helpers/concat.js +1 -1
- package/core/frontend/helpers/content.js +1 -1
- package/core/frontend/helpers/date.js +1 -1
- package/core/frontend/helpers/encode.js +1 -1
- package/core/frontend/helpers/excerpt.js +2 -1
- package/core/frontend/helpers/facebook_url.js +2 -1
- package/core/frontend/helpers/foreach.js +11 -2
- package/core/frontend/helpers/get.js +14 -3
- package/core/frontend/helpers/ghost_foot.js +2 -1
- package/core/frontend/helpers/ghost_head.js +10 -1
- package/core/frontend/helpers/has.js +8 -3
- package/core/frontend/helpers/img_url.js +9 -3
- package/core/frontend/helpers/is.js +7 -2
- package/core/frontend/helpers/lang.js +1 -1
- package/core/frontend/helpers/link.js +11 -2
- package/core/frontend/helpers/link_class.js +11 -2
- package/core/frontend/helpers/match.js +12 -3
- package/core/frontend/helpers/navigation.js +13 -4
- package/core/frontend/helpers/pagination.js +15 -5
- package/core/frontend/helpers/plural.js +8 -2
- package/core/frontend/helpers/post_class.js +1 -1
- package/core/frontend/helpers/prev_post.js +9 -2
- package/core/frontend/helpers/price.js +11 -6
- package/core/frontend/helpers/products.js +2 -1
- package/core/frontend/helpers/reading_time.js +4 -2
- package/core/frontend/helpers/t.js +1 -1
- package/core/frontend/helpers/tags.js +3 -1
- package/core/frontend/helpers/title.js +1 -1
- package/core/frontend/helpers/twitter_url.js +2 -1
- package/core/frontend/helpers/url.js +3 -1
- package/core/frontend/services/proxy.js +34 -57
- package/core/frontend/services/rendering.js +24 -0
- package/core/frontend/services/routing/controllers/channel.js +6 -2
- package/core/frontend/services/routing/controllers/collection.js +6 -2
- package/core/frontend/services/routing/middlewares/page-param.js +6 -2
- package/core/frontend/services/theme-engine/middleware.js +23 -6
- package/core/frontend/services/theme-engine/preview.js +31 -8
- package/core/server/adapters/scheduling/post-scheduling/scheduler-intergation.js +6 -4
- package/core/server/adapters/storage/LocalFileStorage.js +10 -4
- package/core/server/api/canary/custom-theme-settings.js +22 -0
- package/core/server/api/canary/index.js +4 -0
- package/core/server/api/canary/members.js +1 -1
- package/core/server/api/canary/redirects.js +5 -5
- package/core/server/api/canary/settings.js +16 -148
- package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +13 -0
- package/core/server/api/canary/utils/serializers/output/index.js +4 -0
- package/core/server/api/canary/utils/validators/input/settings.js +23 -1
- package/core/server/api/v2/redirects.js +3 -3
- package/core/server/api/v2/settings.js +3 -4
- package/core/server/api/v3/redirects.js +5 -5
- package/core/server/api/v3/settings.js +16 -136
- package/core/server/api/v3/utils/validators/input/settings.js +23 -1
- package/core/server/data/db/state-manager.js +1 -1
- package/core/server/data/exporter/table-lists.js +3 -1
- package/core/server/data/importer/import-manager.js +398 -0
- package/core/server/data/importer/importers/data/data-importer.js +162 -0
- package/core/server/data/importer/importers/data/index.js +1 -162
- package/core/server/data/importer/index.js +1 -379
- package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
- package/core/server/data/migrations/versions/4.17/01-add-custom-theme-settings-permissions.js +21 -0
- package/core/server/data/migrations/versions/4.17/02-add-offers-table.js +19 -0
- package/core/server/data/migrations/versions/4.17/03-add-offers-permissions.js +35 -0
- package/core/server/data/schema/fixtures/fixtures.json +32 -0
- package/core/server/data/schema/schema.js +33 -0
- package/core/server/models/custom-theme-setting.js +9 -0
- package/core/server/models/index.js +2 -0
- package/core/server/services/custom-theme-settings.js +8 -0
- package/core/server/services/members/api.js +4 -1
- package/core/server/services/redirects/index.js +15 -0
- package/core/{frontend → server}/services/redirects/settings.js +13 -6
- package/core/server/services/redirects/validation.js +44 -0
- package/core/{frontend/services/settings → server/services/route-settings}/default-routes.yaml +0 -0
- package/core/server/services/route-settings/default-settings-manager.js +62 -0
- package/core/server/services/route-settings/index.js +32 -1
- package/core/server/services/route-settings/route-settings.js +38 -12
- package/core/server/services/route-settings/settings-loader.js +102 -0
- package/core/{frontend/services/settings → server/services/route-settings}/validate.js +38 -28
- package/core/server/services/route-settings/yaml-parser.js +53 -0
- package/core/server/services/settings/index.js +13 -16
- package/core/server/services/settings/settings-bread-service.js +188 -0
- package/core/server/services/settings/settings-utils.js +32 -0
- package/core/server/services/themes/ThemeStorage.js +5 -4
- package/core/server/services/themes/activation-bridge.js +14 -0
- package/core/server/services/themes/validate.js +5 -2
- 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/canary/admin/routes.js +5 -1
- package/core/server/web/members/app.js +3 -0
- package/core/server/web/oauth/app.js +7 -8
- package/core/server/web/shared/middlewares/custom-redirects.js +82 -59
- package/core/server/web/site/routes.js +2 -2
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/overrides.json +1 -1
- package/core/shared/custom-theme-settings-cache.js +3 -0
- package/core/shared/i18n/translations/en.json +2 -13
- package/core/shared/labs.js +2 -2
- package/package.json +42 -41
- package/yarn.lock +916 -901
- package/core/built/assets/ghost-dark-faf931d90e92535e6c03ca16793cbe7b.css +0 -1
- package/core/built/assets/ghost.min-7aa074ad556a8455155ac88ceaca03ab.css +0 -1
- package/core/frontend/services/redirects/index.js +0 -9
- package/core/frontend/services/redirects/validation.js +0 -28
- package/core/frontend/services/settings/ensure-settings.js +0 -47
- package/core/frontend/services/settings/index.js +0 -104
- package/core/frontend/services/settings/loader.js +0 -89
- package/core/frontend/services/settings/yaml-parser.js +0 -31
|
@@ -3,12 +3,19 @@ const path = require('path');
|
|
|
3
3
|
const moment = require('moment-timezone');
|
|
4
4
|
const yaml = require('js-yaml');
|
|
5
5
|
const Promise = require('bluebird');
|
|
6
|
-
const validation = require('./validation');
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
const i18n = require('../../../shared/i18n');
|
|
7
|
+
const tpl = require('@tryghost/tpl');
|
|
10
8
|
const errors = require('@tryghost/errors');
|
|
11
9
|
|
|
10
|
+
const validation = require('./validation');
|
|
11
|
+
const config = require('../../../shared/config');
|
|
12
|
+
|
|
13
|
+
const messages = {
|
|
14
|
+
jsonParse: 'Could not parse JSON: {context}.',
|
|
15
|
+
yamlParse: 'YAML input cannot be a plain string. Check the format of your YAML file.',
|
|
16
|
+
yamlParseHelp: 'https://ghost.org/docs/themes/routing/#redirects'
|
|
17
|
+
};
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Redirect configuration object
|
|
14
21
|
* @typedef {Object} RedirectConfig
|
|
@@ -49,7 +56,7 @@ const parseRedirectsFile = (content, ext) => {
|
|
|
49
56
|
redirects = JSON.parse(content);
|
|
50
57
|
} catch (err) {
|
|
51
58
|
throw new errors.BadRequestError({
|
|
52
|
-
message:
|
|
59
|
+
message: tpl(messages.jsonParse, {context: err.message})
|
|
53
60
|
});
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -66,8 +73,8 @@ const parseRedirectsFile = (content, ext) => {
|
|
|
66
73
|
// Here we check if the user made this mistake.
|
|
67
74
|
if (typeof configYaml === 'string') {
|
|
68
75
|
throw new errors.BadRequestError({
|
|
69
|
-
message:
|
|
70
|
-
help:
|
|
76
|
+
message: tpl(messages.yamlParse),
|
|
77
|
+
help: tpl(messages.yamlParseHelp)
|
|
71
78
|
});
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
redirectsWrongFormat: 'Incorrect redirects file format.',
|
|
7
|
+
invalidRedirectsFromRegex: 'Incorrect RegEx in redirects file.',
|
|
8
|
+
redirectsHelp: 'https://ghost.org/docs/themes/routing/#redirects'
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Redirects are file based at the moment, but they will live in the database in the future.
|
|
12
|
+
* See V2 of https://github.com/TryGhost/Ghost/issues/7707.
|
|
13
|
+
*/
|
|
14
|
+
const validate = (redirects) => {
|
|
15
|
+
if (!_.isArray(redirects)) {
|
|
16
|
+
throw new errors.ValidationError({
|
|
17
|
+
message: tpl(messages.redirectsWrongFormat),
|
|
18
|
+
help: tpl(messages.redirectsHelp)
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_.each(redirects, function (redirect) {
|
|
23
|
+
if (!redirect.from || !redirect.to) {
|
|
24
|
+
throw new errors.ValidationError({
|
|
25
|
+
message: tpl(messages.redirectsWrongFormat),
|
|
26
|
+
context: redirect,
|
|
27
|
+
help: tpl(messages.redirectsHelp)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// each 'from' property should be a valid RegExp string
|
|
33
|
+
new RegExp(redirect.from);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new errors.ValidationError({
|
|
36
|
+
message: tpl(messages.invalidRedirectsFromRegex),
|
|
37
|
+
context: redirect,
|
|
38
|
+
help: tpl(messages.redirectsHelp)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
module.exports.validate = validate;
|
package/core/{frontend/services/settings → server/services/route-settings}/default-routes.yaml
RENAMED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const Promise = require('bluebird');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const debug = require('@tryghost/debug')('frontend:services:settings:ensure-settings');
|
|
5
|
+
const tpl = require('@tryghost/tpl');
|
|
6
|
+
const errors = require('@tryghost/errors');
|
|
7
|
+
|
|
8
|
+
const messages = {
|
|
9
|
+
ensureSettings: 'Error trying to access settings files in {path}.'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
class DefaultSettingsManager {
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {String} options.type - name of the setting file
|
|
17
|
+
* @param {String} options.extension - settings file extension
|
|
18
|
+
* @param {String} options.destinationFolderPath - path to store the default setting config
|
|
19
|
+
* @param {String} options.sourceFolderPath - path where the default config can be seeded from
|
|
20
|
+
*/
|
|
21
|
+
constructor({type, extension, destinationFolderPath, sourceFolderPath}) {
|
|
22
|
+
this.type = type;
|
|
23
|
+
this.extension = extension;
|
|
24
|
+
this.destinationFolderPath = destinationFolderPath;
|
|
25
|
+
this.sourceFolderPath = sourceFolderPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* Makes sure the destination folder either contains a file or copies over a default file.
|
|
31
|
+
* @returns {Promise<any>}
|
|
32
|
+
*/
|
|
33
|
+
async ensureSettingsFileExists() {
|
|
34
|
+
const fileName = this.type + this.extension;
|
|
35
|
+
const defaultFileName = `default-${fileName}`;
|
|
36
|
+
|
|
37
|
+
const destinationFilePath = path.join(this.destinationFolderPath, fileName);
|
|
38
|
+
const defaultFilePath = path.join(this.sourceFolderPath, defaultFileName);
|
|
39
|
+
|
|
40
|
+
return Promise.resolve(fs.readFile(destinationFilePath, 'utf8'))
|
|
41
|
+
.catch({code: 'ENOENT'}, () => {
|
|
42
|
+
// CASE: file doesn't exist, copy it from our defaults
|
|
43
|
+
return fs.copy(
|
|
44
|
+
defaultFilePath,
|
|
45
|
+
destinationFilePath
|
|
46
|
+
).then(() => {
|
|
47
|
+
debug(`'${defaultFileName}' copied to ${this.destinationFolderPath}.`);
|
|
48
|
+
});
|
|
49
|
+
}).catch((error) => {
|
|
50
|
+
// CASE: we might have a permission error, as we can't access the directory
|
|
51
|
+
throw new errors.GhostError({
|
|
52
|
+
message: tpl(messages.ensureSettings, {
|
|
53
|
+
path: this.destinationFolderPath
|
|
54
|
+
}),
|
|
55
|
+
err: error,
|
|
56
|
+
context: error.path
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = DefaultSettingsManager;
|
|
@@ -1 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
const routeSettings = require('./route-settings');
|
|
2
|
+
const SettingsLoader = require('./settings-loader');
|
|
3
|
+
const config = require('../../../shared/config');
|
|
4
|
+
const parseYaml = require('./yaml-parser');
|
|
5
|
+
const DefaultSettingsManager = require('./default-settings-manager');
|
|
6
|
+
|
|
7
|
+
const defaultSettingsManager = new DefaultSettingsManager({
|
|
8
|
+
type: 'routes',
|
|
9
|
+
extension: '.yaml',
|
|
10
|
+
destinationFolderPath: config.getContentPath('settings'),
|
|
11
|
+
sourceFolderPath: config.get('paths').defaultSettings
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const settingsLoader = new SettingsLoader({parseYaml});
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
init: async () => {
|
|
18
|
+
return await defaultSettingsManager.ensureSettingsFileExists();
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
loadRouteSettingsSync: settingsLoader.loadSettingsSync.bind(settingsLoader),
|
|
22
|
+
loadRouteSettings: settingsLoader.loadSettings.bind(settingsLoader),
|
|
23
|
+
getDefaultHash: routeSettings.getDefaultHash,
|
|
24
|
+
/**
|
|
25
|
+
* Methods used in the API
|
|
26
|
+
*/
|
|
27
|
+
api: {
|
|
28
|
+
setFromFilePath: routeSettings.setFromFilePath,
|
|
29
|
+
get: routeSettings.get,
|
|
30
|
+
getCurrentHash: routeSettings.getCurrentHash
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -2,6 +2,7 @@ const Promise = require('bluebird');
|
|
|
2
2
|
const moment = require('moment-timezone');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const urlService = require('../../../frontend/services/url');
|
|
6
7
|
|
|
7
8
|
const debug = require('@tryghost/debug')('services:route-settings');
|
|
@@ -9,6 +10,10 @@ const errors = require('@tryghost/errors');
|
|
|
9
10
|
const tpl = require('@tryghost/tpl');
|
|
10
11
|
const config = require('../../../shared/config');
|
|
11
12
|
const bridge = require('../../../bridge');
|
|
13
|
+
const SettingsLoader = require('./settings-loader');
|
|
14
|
+
const parseYaml = require('./yaml-parser');
|
|
15
|
+
|
|
16
|
+
const settingsLoader = new SettingsLoader({parseYaml});
|
|
12
17
|
|
|
13
18
|
const messages = {
|
|
14
19
|
loadError: 'Could not load {filename} file.'
|
|
@@ -29,17 +34,13 @@ const messages = {
|
|
|
29
34
|
const filename = 'routes';
|
|
30
35
|
const ext = 'yaml';
|
|
31
36
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const getSettingsFilePath = async () => {
|
|
37
|
-
const settingsFolder = await getSettingsFolder();
|
|
37
|
+
const getSettingsFilePath = () => {
|
|
38
|
+
const settingsFolder = config.getContentPath('settings');
|
|
38
39
|
return path.join(settingsFolder, `${filename}.${ext}`);
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
const getBackupFilePath =
|
|
42
|
-
const settingsFolder =
|
|
42
|
+
const getBackupFilePath = () => {
|
|
43
|
+
const settingsFolder = config.getContentPath('settings');
|
|
43
44
|
return path.join(settingsFolder, `${filename}-${moment().format('YYYY-MM-DD-HH-mm-ss')}.${ext}`);
|
|
44
45
|
};
|
|
45
46
|
|
|
@@ -73,8 +74,8 @@ const readFile = (settingsFilePath) => {
|
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
const setFromFilePath = async (filePath) => {
|
|
76
|
-
const settingsPath =
|
|
77
|
-
const backupPath =
|
|
77
|
+
const settingsPath = getSettingsFilePath();
|
|
78
|
+
const backupPath = getBackupFilePath();
|
|
78
79
|
|
|
79
80
|
await createBackupFile(settingsPath, backupPath);
|
|
80
81
|
await saveFile(filePath, settingsPath);
|
|
@@ -134,5 +135,30 @@ const get = async () => {
|
|
|
134
135
|
return readFile(settingsFilePath);
|
|
135
136
|
};
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* md5 hashes of default routes settings
|
|
140
|
+
*/
|
|
141
|
+
const defaultRoutesSettingHash = '3d180d52c663d173a6be791ef411ed01';
|
|
142
|
+
|
|
143
|
+
const calculateHash = (data) => {
|
|
144
|
+
return crypto.createHash('md5')
|
|
145
|
+
.update(data, 'binary')
|
|
146
|
+
.digest('hex');
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const getDefaultHash = () => {
|
|
150
|
+
return defaultRoutesSettingHash;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const getCurrentHash = async () => {
|
|
154
|
+
const data = await settingsLoader.loadSettings();
|
|
155
|
+
|
|
156
|
+
return calculateHash(JSON.stringify(data));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
getDefaultHash: getDefaultHash,
|
|
161
|
+
setFromFilePath: setFromFilePath,
|
|
162
|
+
get: get,
|
|
163
|
+
getCurrentHash: getCurrentHash
|
|
164
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const debug = require('@tryghost/debug')('frontend:services:settings:settings-loader');
|
|
4
|
+
const tpl = require('@tryghost/tpl');
|
|
5
|
+
const errors = require('@tryghost/errors');
|
|
6
|
+
const config = require('../../../shared/config');
|
|
7
|
+
const validate = require('./validate');
|
|
8
|
+
|
|
9
|
+
const messages = {
|
|
10
|
+
settingsLoaderError: `Error trying to load YAML setting for {setting} from '{path}'.`
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class SettingsLoader {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {Function} options.parseYaml yaml parser
|
|
17
|
+
*/
|
|
18
|
+
constructor({parseYaml}) {
|
|
19
|
+
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
|
+
|
|
33
|
+
return filePath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Functionally same as loadSettingsSync with exception of loading
|
|
38
|
+
* settings asynchronously. This method is used at new places to read settings
|
|
39
|
+
* to prevent blocking the eventloop
|
|
40
|
+
* @returns {Promise<Object>} settingsFile
|
|
41
|
+
*/
|
|
42
|
+
async loadSettings() {
|
|
43
|
+
const setting = 'routes';
|
|
44
|
+
const filePath = this.getSettingFilePath(setting);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const file = await fs.readFile(filePath, 'utf8');
|
|
48
|
+
debug('settings file found for', setting);
|
|
49
|
+
|
|
50
|
+
const object = this.parseYaml(file);
|
|
51
|
+
|
|
52
|
+
debug('YAML settings file parsed:', filePath);
|
|
53
|
+
|
|
54
|
+
return validate(object);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (errors.utils.isIgnitionError(err)) {
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new errors.GhostError({
|
|
61
|
+
message: tpl(messages.settingsLoaderError, {
|
|
62
|
+
setting: setting,
|
|
63
|
+
path: filePath
|
|
64
|
+
}),
|
|
65
|
+
err: err
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reads the routes.yaml settings file and passes the
|
|
72
|
+
* file to the YAML parser which then returns a JSON object.
|
|
73
|
+
*
|
|
74
|
+
* @returns {Object} settingsFile in following format: {routes: {}, collections: {}, resources: {}}
|
|
75
|
+
*/
|
|
76
|
+
loadSettingsSync() {
|
|
77
|
+
const setting = 'routes';
|
|
78
|
+
const filePath = this.getSettingFilePath(setting);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const file = fs.readFileSync(filePath, 'utf8');
|
|
82
|
+
debug('settings file found for', setting);
|
|
83
|
+
|
|
84
|
+
const object = this.parseYaml(file);
|
|
85
|
+
return validate(object);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (errors.utils.isIgnitionError(err)) {
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw new errors.GhostError({
|
|
92
|
+
message: tpl(messages.settingsLoaderError, {
|
|
93
|
+
setting: setting,
|
|
94
|
+
path: filePath
|
|
95
|
+
}),
|
|
96
|
+
err: err
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = SettingsLoader;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('frontend:services:settings:validate');
|
|
3
|
-
const
|
|
3
|
+
const tpl = require('@tryghost/tpl');
|
|
4
4
|
const errors = require('@tryghost/errors');
|
|
5
5
|
const bridge = require('../../../bridge');
|
|
6
6
|
const _private = {};
|
|
7
7
|
let RESOURCE_CONFIG;
|
|
8
8
|
|
|
9
|
+
const messages = {
|
|
10
|
+
validationError: 'The following definition "{at}" is invalid: {reason}',
|
|
11
|
+
invalidResourceError: 'Resource key not supported. {resourceKey}',
|
|
12
|
+
invalidResourceHelp: 'Please use: tag, user, post or page.',
|
|
13
|
+
badDatError: 'Please wrap the data definition into a custom name.',
|
|
14
|
+
badDataHelp: 'Example:\n data:\n my-tag:\n resource: tags\n ...\n',
|
|
15
|
+
authorDeprecatedError: 'Please choose a different name. We recommend not using author.'
|
|
16
|
+
};
|
|
17
|
+
|
|
9
18
|
_private.validateTemplate = function validateTemplate(object) {
|
|
10
19
|
// CASE: /about/: about
|
|
11
20
|
if (typeof object === 'string') {
|
|
@@ -42,7 +51,7 @@ _private.validateData = function validateData(object) {
|
|
|
42
51
|
|
|
43
52
|
if (!shortForm.match(/.*\..*/)) {
|
|
44
53
|
throw new errors.ValidationError({
|
|
45
|
-
message:
|
|
54
|
+
message: tpl(messages.validationError, {
|
|
46
55
|
at: shortForm,
|
|
47
56
|
reason: 'Incorrect Format. Please use e.g. tag.recipes'
|
|
48
57
|
})
|
|
@@ -54,8 +63,8 @@ _private.validateData = function validateData(object) {
|
|
|
54
63
|
if (!RESOURCE_CONFIG.QUERY[resourceKey] ||
|
|
55
64
|
(Object.prototype.hasOwnProperty.call(RESOURCE_CONFIG.QUERY[resourceKey], 'internal') && RESOURCE_CONFIG.QUERY[resourceKey].internal === true)) {
|
|
56
65
|
throw new errors.ValidationError({
|
|
57
|
-
message:
|
|
58
|
-
help:
|
|
66
|
+
message: tpl(messages.invalidResourceError, {resourceKey}),
|
|
67
|
+
help: tpl(messages.invalidResourceHelp)
|
|
59
68
|
});
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -98,15 +107,15 @@ _private.validateData = function validateData(object) {
|
|
|
98
107
|
// CASE: a name is required to define the data longform
|
|
99
108
|
if (['resource', 'type', 'limit', 'order', 'include', 'filter', 'status', 'visibility', 'slug', 'redirect'].indexOf(key) !== -1) {
|
|
100
109
|
throw new errors.ValidationError({
|
|
101
|
-
message:
|
|
102
|
-
help:
|
|
110
|
+
message: tpl(messages.badDataError),
|
|
111
|
+
help: tpl(messages.badDataHelp)
|
|
103
112
|
});
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
// @NOTE: We disallow author, because {{author}} is deprecated.
|
|
107
116
|
if (key === 'author') {
|
|
108
117
|
throw new errors.ValidationError({
|
|
109
|
-
message:
|
|
118
|
+
message: tpl(messages.authorDeprecatedError)
|
|
110
119
|
});
|
|
111
120
|
}
|
|
112
121
|
|
|
@@ -133,7 +142,7 @@ _private.validateData = function validateData(object) {
|
|
|
133
142
|
_.each(requiredQueryFields, (option) => {
|
|
134
143
|
if (!Object.prototype.hasOwnProperty.call(object.data[key], option)) {
|
|
135
144
|
throw new errors.ValidationError({
|
|
136
|
-
message:
|
|
145
|
+
message: tpl(messages.validationError, {
|
|
137
146
|
at: JSON.stringify(object.data[key]),
|
|
138
147
|
reason: `${option} is required.`
|
|
139
148
|
})
|
|
@@ -142,7 +151,7 @@ _private.validateData = function validateData(object) {
|
|
|
142
151
|
|
|
143
152
|
if (allowedQueryValues[option] && allowedQueryValues[option].indexOf(object.data[key][option]) === -1) {
|
|
144
153
|
throw new errors.ValidationError({
|
|
145
|
-
message:
|
|
154
|
+
message: tpl(messages.validationError, {
|
|
146
155
|
at: JSON.stringify(object.data[key]),
|
|
147
156
|
reason: `${object.data[key][option]} not supported. Please use ${_.uniq(allowedQueryValues[option])}.`
|
|
148
157
|
})
|
|
@@ -186,7 +195,7 @@ _private.validateData = function validateData(object) {
|
|
|
186
195
|
_private.validateRoutes = function validateRoutes(routes) {
|
|
187
196
|
if (routes.constructor !== Object) {
|
|
188
197
|
throw new errors.ValidationError({
|
|
189
|
-
message:
|
|
198
|
+
message: tpl(messages.validationError, {
|
|
190
199
|
at: routes,
|
|
191
200
|
reason: '`routes` must be a YAML map.'
|
|
192
201
|
})
|
|
@@ -197,7 +206,7 @@ _private.validateRoutes = function validateRoutes(routes) {
|
|
|
197
206
|
// CASE: we hard-require trailing slashes for the index route
|
|
198
207
|
if (!routingTypeObjectKey.match(/\/$/)) {
|
|
199
208
|
throw new errors.ValidationError({
|
|
200
|
-
message:
|
|
209
|
+
message: tpl(messages.validationError, {
|
|
201
210
|
at: routingTypeObjectKey,
|
|
202
211
|
reason: 'A trailing slash is required.'
|
|
203
212
|
})
|
|
@@ -207,7 +216,7 @@ _private.validateRoutes = function validateRoutes(routes) {
|
|
|
207
216
|
// CASE: we hard-require leading slashes for the index route
|
|
208
217
|
if (!routingTypeObjectKey.match(/^\//)) {
|
|
209
218
|
throw new errors.ValidationError({
|
|
210
|
-
message:
|
|
219
|
+
message: tpl(messages.validationError, {
|
|
211
220
|
at: routingTypeObjectKey,
|
|
212
221
|
reason: 'A leading slash is required.'
|
|
213
222
|
})
|
|
@@ -217,7 +226,7 @@ _private.validateRoutes = function validateRoutes(routes) {
|
|
|
217
226
|
// CASE: you define /about/:
|
|
218
227
|
if (!routingTypeObject) {
|
|
219
228
|
throw new errors.ValidationError({
|
|
220
|
-
message:
|
|
229
|
+
message: tpl(messages.validationError, {
|
|
221
230
|
at: routingTypeObjectKey,
|
|
222
231
|
reason: 'Please define a template.'
|
|
223
232
|
}),
|
|
@@ -235,7 +244,7 @@ _private.validateRoutes = function validateRoutes(routes) {
|
|
|
235
244
|
_private.validateCollections = function validateCollections(collections) {
|
|
236
245
|
if (collections.constructor !== Object) {
|
|
237
246
|
throw new errors.ValidationError({
|
|
238
|
-
message:
|
|
247
|
+
message: tpl(messages.validationError, {
|
|
239
248
|
at: collections,
|
|
240
249
|
reason: '`collections` must be a YAML map.'
|
|
241
250
|
})
|
|
@@ -246,7 +255,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
246
255
|
// CASE: we hard-require trailing slashes for the collection index route
|
|
247
256
|
if (!routingTypeObjectKey.match(/\/$/)) {
|
|
248
257
|
throw new errors.ValidationError({
|
|
249
|
-
message:
|
|
258
|
+
message: tpl(messages.validationError, {
|
|
250
259
|
at: routingTypeObjectKey,
|
|
251
260
|
reason: 'A trailing slash is required.'
|
|
252
261
|
})
|
|
@@ -256,7 +265,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
256
265
|
// CASE: we hard-require leading slashes for the collection index route
|
|
257
266
|
if (!routingTypeObjectKey.match(/^\//)) {
|
|
258
267
|
throw new errors.ValidationError({
|
|
259
|
-
message:
|
|
268
|
+
message: tpl(messages.validationError, {
|
|
260
269
|
at: routingTypeObjectKey,
|
|
261
270
|
reason: 'A leading slash is required.'
|
|
262
271
|
})
|
|
@@ -265,7 +274,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
265
274
|
|
|
266
275
|
if (!Object.prototype.hasOwnProperty.call(routingTypeObject, 'permalink')) {
|
|
267
276
|
throw new errors.ValidationError({
|
|
268
|
-
message:
|
|
277
|
+
message: tpl(messages.validationError, {
|
|
269
278
|
at: routingTypeObjectKey,
|
|
270
279
|
reason: 'Please define a permalink route.'
|
|
271
280
|
}),
|
|
@@ -277,7 +286,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
277
286
|
|
|
278
287
|
if (!routingTypeObject.permalink) {
|
|
279
288
|
throw new errors.ValidationError({
|
|
280
|
-
message:
|
|
289
|
+
message: tpl(messages.validationError, {
|
|
281
290
|
at: routingTypeObjectKey,
|
|
282
291
|
reason: 'Please define a permalink route.'
|
|
283
292
|
}),
|
|
@@ -288,7 +297,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
288
297
|
// CASE: we hard-require trailing slashes for the value/permalink route
|
|
289
298
|
if (!routingTypeObject.permalink.match(/\/$/)) {
|
|
290
299
|
throw new errors.ValidationError({
|
|
291
|
-
message:
|
|
300
|
+
message: tpl(messages.validationError, {
|
|
292
301
|
at: routingTypeObject.permalink,
|
|
293
302
|
reason: 'A trailing slash is required.'
|
|
294
303
|
})
|
|
@@ -298,7 +307,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
298
307
|
// CASE: we hard-require leading slashes for the value/permalink route
|
|
299
308
|
if (!routingTypeObject.permalink.match(/^\//)) {
|
|
300
309
|
throw new errors.ValidationError({
|
|
301
|
-
message:
|
|
310
|
+
message: tpl(messages.validationError, {
|
|
302
311
|
at: routingTypeObject.permalink,
|
|
303
312
|
reason: 'A leading slash is required.'
|
|
304
313
|
})
|
|
@@ -308,7 +317,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
308
317
|
// CASE: notation /:slug/ or /:primary_author/ is not allowed. We only accept /{{...}}/.
|
|
309
318
|
if (routingTypeObject.permalink && routingTypeObject.permalink.match(/\/:\w+/)) {
|
|
310
319
|
throw new errors.ValidationError({
|
|
311
|
-
message:
|
|
320
|
+
message: tpl(messages.validationError, {
|
|
312
321
|
at: routingTypeObject.permalink,
|
|
313
322
|
reason: 'Please use the following notation e.g. /{slug}/.'
|
|
314
323
|
})
|
|
@@ -331,7 +340,7 @@ _private.validateCollections = function validateCollections(collections) {
|
|
|
331
340
|
_private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
|
332
341
|
if (taxonomies.constructor !== Object) {
|
|
333
342
|
throw new errors.ValidationError({
|
|
334
|
-
message:
|
|
343
|
+
message: tpl(messages.validationError, {
|
|
335
344
|
at: taxonomies,
|
|
336
345
|
reason: '`taxonomies` must be a YAML map.'
|
|
337
346
|
})
|
|
@@ -342,7 +351,7 @@ _private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
|
|
342
351
|
_.each(taxonomies, (routingTypeObject, routingTypeObjectKey) => {
|
|
343
352
|
if (!routingTypeObject) {
|
|
344
353
|
throw new errors.ValidationError({
|
|
345
|
-
message:
|
|
354
|
+
message: tpl(messages.validationError, {
|
|
346
355
|
at: routingTypeObjectKey,
|
|
347
356
|
reason: 'Please define a taxonomy permalink route.'
|
|
348
357
|
}),
|
|
@@ -352,7 +361,7 @@ _private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
|
|
352
361
|
|
|
353
362
|
if (!validRoutingTypeObjectKeys.includes(routingTypeObjectKey)) {
|
|
354
363
|
throw new errors.ValidationError({
|
|
355
|
-
message:
|
|
364
|
+
message: tpl(messages.validationError, {
|
|
356
365
|
at: routingTypeObjectKey,
|
|
357
366
|
reason: 'Unknown taxonomy.'
|
|
358
367
|
})
|
|
@@ -362,7 +371,7 @@ _private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
|
|
362
371
|
// CASE: we hard-require trailing slashes for the taxonomie permalink route
|
|
363
372
|
if (!routingTypeObject.match(/\/$/)) {
|
|
364
373
|
throw new errors.ValidationError({
|
|
365
|
-
message:
|
|
374
|
+
message: tpl(messages.validationError, {
|
|
366
375
|
at: routingTypeObject,
|
|
367
376
|
reason: 'A trailing slash is required.'
|
|
368
377
|
})
|
|
@@ -372,7 +381,7 @@ _private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
|
|
372
381
|
// CASE: we hard-require leading slashes for the value/permalink route
|
|
373
382
|
if (!routingTypeObject.match(/^\//)) {
|
|
374
383
|
throw new errors.ValidationError({
|
|
375
|
-
message:
|
|
384
|
+
message: tpl(messages.validationError, {
|
|
376
385
|
at: routingTypeObject,
|
|
377
386
|
reason: 'A leading slash is required.'
|
|
378
387
|
})
|
|
@@ -382,7 +391,7 @@ _private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
|
|
382
391
|
// CASE: notation /:slug/ or /:primary_author/ is not allowed. We only accept /{{...}}/.
|
|
383
392
|
if (routingTypeObject && routingTypeObject.match(/\/:\w+/)) {
|
|
384
393
|
throw new errors.ValidationError({
|
|
385
|
-
message:
|
|
394
|
+
message: tpl(messages.validationError, {
|
|
386
395
|
at: routingTypeObject,
|
|
387
396
|
reason: 'Please use the following notation e.g. /{slug}/.'
|
|
388
397
|
})
|
|
@@ -425,7 +434,8 @@ module.exports = function validate(object) {
|
|
|
425
434
|
|
|
426
435
|
debug('api version', apiVersion);
|
|
427
436
|
|
|
428
|
-
|
|
437
|
+
// TODO: extract this config outta here! the config should be passed into this module
|
|
438
|
+
RESOURCE_CONFIG = require(`../../../frontend/services/routing/config/${apiVersion}`);
|
|
429
439
|
|
|
430
440
|
object.routes = _private.validateRoutes(object.routes);
|
|
431
441
|
object.collections = _private.validateCollections(object.collections);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const yaml = require('js-yaml');
|
|
2
|
+
const tpl = require('@tryghost/tpl');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
parsingError: {
|
|
7
|
+
message: 'Could not parse provided YAML file: {context}.',
|
|
8
|
+
help: 'Check provided file for typos and fix the named issues.'
|
|
9
|
+
},
|
|
10
|
+
invalidYamlFormat: {
|
|
11
|
+
message: 'YAML input cannot be a plain string. Check the format of your YAML file.',
|
|
12
|
+
help: 'https://ghost.org/docs/themes/routing/'
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Takes a YAML formatted string and parses it and returns a JSON Object
|
|
18
|
+
* @param {String} file the YAML file utf8 encoded
|
|
19
|
+
* @returns {Object} parsed
|
|
20
|
+
*/
|
|
21
|
+
module.exports = function parseYaml(file) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = yaml.load(file);
|
|
24
|
+
|
|
25
|
+
// yaml.load passes almost every yaml code.
|
|
26
|
+
// Because of that, it's hard to detect if there's an error in the file.
|
|
27
|
+
// But one of the obvious errors is the plain string output.
|
|
28
|
+
// Here we check if the user made this mistake.
|
|
29
|
+
if (typeof parsed === 'string') {
|
|
30
|
+
throw new errors.IncorrectUsageError({
|
|
31
|
+
message: messages.invalidYamlFormat.message,
|
|
32
|
+
help: messages.invalidYamlFormat.help
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parsed;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (errors.utils.isIgnitionError(error)) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// CASE: parsing failed, `js-yaml` tells us exactly what and where in the
|
|
43
|
+
// `reason` property as well as in the message.
|
|
44
|
+
// As the file uploaded is invalid, the person uploading must fix this - it's a 4xx error
|
|
45
|
+
throw new errors.IncorrectUsageError({
|
|
46
|
+
message: tpl(messages.parsingError.message, {context: error.reason}),
|
|
47
|
+
code: 'YAML_PARSER_ERROR',
|
|
48
|
+
context: error.message,
|
|
49
|
+
err: error,
|
|
50
|
+
help: messages.parsingError.help
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|