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.
Files changed (137) hide show
  1. package/.eslintrc.js +7 -1
  2. package/content/themes/casper/assets/built/screen.css +1 -1
  3. package/content/themes/casper/assets/built/screen.css.map +1 -1
  4. package/content/themes/casper/assets/css/screen.css +1 -1
  5. package/content/themes/casper/default.hbs +2 -2
  6. package/content/themes/casper/package.json +1 -1
  7. package/content/themes/casper/page.hbs +28 -26
  8. package/content/themes/casper/partials/post-card.hbs +2 -2
  9. package/content/themes/casper/post.hbs +67 -65
  10. package/content/themes/casper/tag.hbs +2 -2
  11. package/core/boot.js +7 -7
  12. package/core/bridge.js +4 -3
  13. package/core/built/assets/{chunk.3.4b1d9e20e57164ac9c29.js → chunk.3.b80d3e1e6b8556aaff3c.js} +72 -71
  14. package/core/built/assets/ghost-dark-f7bf2dd8d8c702716f75bfa4ccd92df2.css +1 -0
  15. package/core/built/assets/{ghost.min-e35cfee26d942c364166f57f3dcc9e75.js → ghost.min-52a5420ffcea6bf17761b5c59cf020e2.js} +979 -908
  16. package/core/built/assets/ghost.min-741246f42f000c073999a5363434ea2c.css +1 -0
  17. package/core/built/assets/icons/discount-bubble.svg +1 -0
  18. package/core/built/assets/{vendor.min-ca33abc718f21a51327841d58f8875d0.js → vendor.min-1bfc9d56d27508db88ef417deb55f16f.js} +454 -434
  19. package/core/frontend/apps/amp/lib/helpers/amp_analytics.js +2 -2
  20. package/core/frontend/apps/amp/lib/helpers/amp_components.js +2 -1
  21. package/core/frontend/apps/amp/lib/helpers/amp_content.js +5 -1
  22. package/core/frontend/apps/amp/lib/helpers/amp_style.js +1 -1
  23. package/core/frontend/apps/amp/lib/router.js +8 -4
  24. package/core/frontend/apps/private-blogging/index.js +13 -5
  25. package/core/frontend/apps/private-blogging/lib/helpers/input_password.js +1 -1
  26. package/core/frontend/apps/private-blogging/lib/middleware.js +8 -3
  27. package/core/frontend/helpers/asset.js +10 -2
  28. package/core/frontend/helpers/author.js +5 -3
  29. package/core/frontend/helpers/authors.js +4 -3
  30. package/core/frontend/helpers/body_class.js +1 -1
  31. package/core/frontend/helpers/cancel_link.js +9 -2
  32. package/core/frontend/helpers/concat.js +1 -1
  33. package/core/frontend/helpers/content.js +1 -1
  34. package/core/frontend/helpers/date.js +1 -1
  35. package/core/frontend/helpers/encode.js +1 -1
  36. package/core/frontend/helpers/excerpt.js +2 -1
  37. package/core/frontend/helpers/facebook_url.js +2 -1
  38. package/core/frontend/helpers/foreach.js +11 -2
  39. package/core/frontend/helpers/get.js +14 -3
  40. package/core/frontend/helpers/ghost_foot.js +2 -1
  41. package/core/frontend/helpers/ghost_head.js +10 -1
  42. package/core/frontend/helpers/has.js +8 -3
  43. package/core/frontend/helpers/img_url.js +9 -3
  44. package/core/frontend/helpers/is.js +7 -2
  45. package/core/frontend/helpers/lang.js +1 -1
  46. package/core/frontend/helpers/link.js +11 -2
  47. package/core/frontend/helpers/link_class.js +11 -2
  48. package/core/frontend/helpers/match.js +12 -3
  49. package/core/frontend/helpers/navigation.js +13 -4
  50. package/core/frontend/helpers/pagination.js +15 -5
  51. package/core/frontend/helpers/plural.js +8 -2
  52. package/core/frontend/helpers/post_class.js +1 -1
  53. package/core/frontend/helpers/prev_post.js +9 -2
  54. package/core/frontend/helpers/price.js +11 -6
  55. package/core/frontend/helpers/products.js +2 -1
  56. package/core/frontend/helpers/reading_time.js +4 -2
  57. package/core/frontend/helpers/t.js +1 -1
  58. package/core/frontend/helpers/tags.js +3 -1
  59. package/core/frontend/helpers/title.js +1 -1
  60. package/core/frontend/helpers/twitter_url.js +2 -1
  61. package/core/frontend/helpers/url.js +3 -1
  62. package/core/frontend/services/proxy.js +34 -57
  63. package/core/frontend/services/rendering.js +24 -0
  64. package/core/frontend/services/routing/controllers/channel.js +6 -2
  65. package/core/frontend/services/routing/controllers/collection.js +6 -2
  66. package/core/frontend/services/routing/middlewares/page-param.js +6 -2
  67. package/core/frontend/services/theme-engine/middleware.js +23 -6
  68. package/core/frontend/services/theme-engine/preview.js +31 -8
  69. package/core/server/adapters/scheduling/post-scheduling/scheduler-intergation.js +6 -4
  70. package/core/server/adapters/storage/LocalFileStorage.js +10 -4
  71. package/core/server/api/canary/custom-theme-settings.js +22 -0
  72. package/core/server/api/canary/index.js +4 -0
  73. package/core/server/api/canary/members.js +1 -1
  74. package/core/server/api/canary/redirects.js +5 -5
  75. package/core/server/api/canary/settings.js +16 -148
  76. package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +13 -0
  77. package/core/server/api/canary/utils/serializers/output/index.js +4 -0
  78. package/core/server/api/canary/utils/validators/input/settings.js +23 -1
  79. package/core/server/api/v2/redirects.js +3 -3
  80. package/core/server/api/v2/settings.js +3 -4
  81. package/core/server/api/v3/redirects.js +5 -5
  82. package/core/server/api/v3/settings.js +16 -136
  83. package/core/server/api/v3/utils/validators/input/settings.js +23 -1
  84. package/core/server/data/db/state-manager.js +1 -1
  85. package/core/server/data/exporter/table-lists.js +3 -1
  86. package/core/server/data/importer/import-manager.js +398 -0
  87. package/core/server/data/importer/importers/data/data-importer.js +162 -0
  88. package/core/server/data/importer/importers/data/index.js +1 -162
  89. package/core/server/data/importer/index.js +1 -379
  90. package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
  91. package/core/server/data/migrations/versions/4.17/01-add-custom-theme-settings-permissions.js +21 -0
  92. package/core/server/data/migrations/versions/4.17/02-add-offers-table.js +19 -0
  93. package/core/server/data/migrations/versions/4.17/03-add-offers-permissions.js +35 -0
  94. package/core/server/data/schema/fixtures/fixtures.json +32 -0
  95. package/core/server/data/schema/schema.js +33 -0
  96. package/core/server/models/custom-theme-setting.js +9 -0
  97. package/core/server/models/index.js +2 -0
  98. package/core/server/services/custom-theme-settings.js +8 -0
  99. package/core/server/services/members/api.js +4 -1
  100. package/core/server/services/redirects/index.js +15 -0
  101. package/core/{frontend → server}/services/redirects/settings.js +13 -6
  102. package/core/server/services/redirects/validation.js +44 -0
  103. package/core/{frontend/services/settings → server/services/route-settings}/default-routes.yaml +0 -0
  104. package/core/server/services/route-settings/default-settings-manager.js +62 -0
  105. package/core/server/services/route-settings/index.js +32 -1
  106. package/core/server/services/route-settings/route-settings.js +38 -12
  107. package/core/server/services/route-settings/settings-loader.js +102 -0
  108. package/core/{frontend/services/settings → server/services/route-settings}/validate.js +38 -28
  109. package/core/server/services/route-settings/yaml-parser.js +53 -0
  110. package/core/server/services/settings/index.js +13 -16
  111. package/core/server/services/settings/settings-bread-service.js +188 -0
  112. package/core/server/services/settings/settings-utils.js +32 -0
  113. package/core/server/services/themes/ThemeStorage.js +5 -4
  114. package/core/server/services/themes/activation-bridge.js +14 -0
  115. package/core/server/services/themes/validate.js +5 -2
  116. package/core/server/web/admin/views/default-prod.html +4 -4
  117. package/core/server/web/admin/views/default.html +4 -4
  118. package/core/server/web/api/canary/admin/routes.js +5 -1
  119. package/core/server/web/members/app.js +3 -0
  120. package/core/server/web/oauth/app.js +7 -8
  121. package/core/server/web/shared/middlewares/custom-redirects.js +82 -59
  122. package/core/server/web/site/routes.js +2 -2
  123. package/core/shared/config/defaults.json +2 -2
  124. package/core/shared/config/overrides.json +1 -1
  125. package/core/shared/custom-theme-settings-cache.js +3 -0
  126. package/core/shared/i18n/translations/en.json +2 -13
  127. package/core/shared/labs.js +2 -2
  128. package/package.json +42 -41
  129. package/yarn.lock +916 -901
  130. package/core/built/assets/ghost-dark-faf931d90e92535e6c03ca16793cbe7b.css +0 -1
  131. package/core/built/assets/ghost.min-7aa074ad556a8455155ac88ceaca03ab.css +0 -1
  132. package/core/frontend/services/redirects/index.js +0 -9
  133. package/core/frontend/services/redirects/validation.js +0 -28
  134. package/core/frontend/services/settings/ensure-settings.js +0 -47
  135. package/core/frontend/services/settings/index.js +0 -104
  136. package/core/frontend/services/settings/loader.js +0 -89
  137. 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 config = require('../../../shared/config');
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: i18n.t('errors.general.jsonParse', {context: err.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: i18n.t('errors.api.redirects.yamlParse'),
70
- help: 'https://ghost.org/docs/themes/routing/#redirects'
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;
@@ -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
- module.exports = require('./route-settings');
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 getSettingsFolder = async () => {
33
- return config.getContentPath('settings');
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 = async () => {
42
- const settingsFolder = await getSettingsFolder();
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 = await getSettingsFilePath();
77
- const backupPath = await getBackupFilePath();
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
- module.exports.setFromFilePath = setFromFilePath;
138
- module.exports.get = get;
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 {i18n} = require('../proxy');
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: i18n.t('errors.services.settings.yaml.validate', {
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: `Resource key not supported. ${resourceKey}`,
58
- help: 'Please use: tag, user, post or page.'
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: 'Please wrap the data definition into a custom name.',
102
- help: 'Example:\n data:\n my-tag:\n resource: tags\n ...\n'
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: 'Please choose a different name. We recommend not using author.'
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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: i18n.t('errors.services.settings.yaml.validate', {
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
- RESOURCE_CONFIG = require(`../routing/config/${apiVersion}`);
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
+ };