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
@@ -1,10 +1,19 @@
1
1
  // ### Navigation Helper
2
2
  // `{{navigation}}`
3
3
  // Outputs navigation menu of static urls
4
+ const {SafeString, templates, hbs} = require('../services/rendering');
4
5
 
5
- const {SafeString, i18n, errors, templates, hbs} = require('../services/proxy');
6
+ const errors = require('@tryghost/errors');
7
+ const tpl = require('@tryghost/tpl');
6
8
  const {slugify} = require('@tryghost/string');
7
9
  const _ = require('lodash');
10
+
11
+ const messages = {
12
+ invalidData: 'navigation data is not an object or is a function',
13
+ valuesMustBeDefined: 'All values must be defined for label, url and current',
14
+ valuesMustBeString: 'Invalid value, Url and Label must be strings'
15
+ };
16
+
8
17
  const createFrame = hbs.handlebars.createFrame;
9
18
 
10
19
  module.exports = function navigation(options) {
@@ -25,7 +34,7 @@ module.exports = function navigation(options) {
25
34
 
26
35
  if (!_.isObject(navigationData) || _.isFunction(navigationData)) {
27
36
  throw new errors.IncorrectUsageError({
28
- message: i18n.t('warnings.helpers.navigation.invalidData')
37
+ message: tpl(messages.invalidData)
29
38
  });
30
39
  }
31
40
 
@@ -33,7 +42,7 @@ module.exports = function navigation(options) {
33
42
  return (_.isUndefined(e.label) || _.isUndefined(e.url));
34
43
  }).length > 0) {
35
44
  throw new errors.IncorrectUsageError({
36
- message: i18n.t('warnings.helpers.navigation.valuesMustBeDefined')
45
+ message: tpl(messages.valuesMustBeDefined)
37
46
  });
38
47
  }
39
48
 
@@ -43,7 +52,7 @@ module.exports = function navigation(options) {
43
52
  (!_.isNull(e.url) && !_.isString(e.url)));
44
53
  }).length > 0) {
45
54
  throw new errors.IncorrectUsageError({
46
- message: i18n.t('warnings.helpers.navigation.valuesMustBeString')
55
+ message: tpl(messages.valuesMustBeString)
47
56
  });
48
57
  }
49
58
 
@@ -1,9 +1,19 @@
1
1
  // ### Pagination Helper
2
2
  // `{{pagination}}`
3
3
  // Outputs previous and next buttons, along with info about the current page
4
+ const {templates, hbs} = require('../services/rendering');
4
5
 
5
- const {errors, i18n, templates, hbs} = require('../services/proxy');
6
+ const errors = require('@tryghost/errors');
7
+ const tpl = require('@tryghost/tpl');
6
8
  const _ = require('lodash');
9
+
10
+ const messages = {
11
+ invalidData: 'The {{pagination}} helper was used outside of a paginated context. See https://ghost.org/docs/themes/helpers/pagination/.',
12
+ valuesMustBeDefined: 'All values must be defined for page, pages, limit and total',
13
+ nextPrevValuesMustBeNumeric: 'Invalid value, Next/Prev must be a number',
14
+ valuesMustBeNumeric: 'Invalid value, check page, pages, limit and total are numbers'
15
+ };
16
+
7
17
  const createFrame = hbs.handlebars.createFrame;
8
18
 
9
19
  module.exports = function pagination(options) {
@@ -14,7 +24,7 @@ module.exports = function pagination(options) {
14
24
  if (!_.isObject(this.pagination) || _.isFunction(this.pagination)) {
15
25
  throw new errors.IncorrectUsageError({
16
26
  level: 'normal',
17
- message: i18n.t('warnings.helpers.pagination.invalidData'),
27
+ message: tpl(messages.invalidData),
18
28
  help: 'https://ghost.org/docs/themes/helpers/pagination/'
19
29
  });
20
30
  }
@@ -22,20 +32,20 @@ module.exports = function pagination(options) {
22
32
  if (_.isUndefined(this.pagination.page) || _.isUndefined(this.pagination.pages) ||
23
33
  _.isUndefined(this.pagination.total) || _.isUndefined(this.pagination.limit)) {
24
34
  throw new errors.IncorrectUsageError({
25
- message: i18n.t('warnings.helpers.pagination.valuesMustBeDefined')
35
+ message: tpl(messages.valuesMustBeDefined)
26
36
  });
27
37
  }
28
38
 
29
39
  if ((!_.isNull(this.pagination.next) && !_.isNumber(this.pagination.next)) ||
30
40
  (!_.isNull(this.pagination.prev) && !_.isNumber(this.pagination.prev))) {
31
41
  throw new errors.IncorrectUsageError({
32
- message: i18n.t('warnings.helpers.pagination.nextPrevValuesMustBeNumeric')
42
+ message: tpl(messages.nextPrevValuesMustBeNumeric)
33
43
  });
34
44
  }
35
45
 
36
46
  if (!_.isNumber(this.pagination.page) || !_.isNumber(this.pagination.pages) ||
37
47
  !_.isNumber(this.pagination.total) || !_.isNumber(this.pagination.limit)) {
38
- throw new errors.IncorrectUsageError({message: i18n.t('warnings.helpers.pagination.valuesMustBeNumeric')});
48
+ throw new errors.IncorrectUsageError({message: tpl(messages.valuesMustBeNumeric)});
39
49
  }
40
50
 
41
51
  // CASE: The pagination helper should have access to the pagination properties at the top level.
@@ -9,15 +9,21 @@
9
9
  // The 2nd argument is the string that will be output if the variable's value is 0
10
10
  // The 3rd argument is the string that will be output if the variable's value is 1
11
11
  // The 4th argument is the string that will be output if the variable's value is 2+
12
+ const {SafeString} = require('../services/rendering');
12
13
 
13
- const {errors, i18n, SafeString} = require('../services/proxy');
14
+ const errors = require('@tryghost/errors');
15
+ const tpl = require('@tryghost/tpl');
14
16
  const isUndefined = require('lodash/isUndefined');
15
17
 
18
+ const messages = {
19
+ valuesMustBeDefined: 'All values must be defined for empty, singular and plural'
20
+ };
21
+
16
22
  module.exports = function plural(number, options) {
17
23
  if (isUndefined(options.hash) || isUndefined(options.hash.empty) ||
18
24
  isUndefined(options.hash.singular) || isUndefined(options.hash.plural)) {
19
25
  throw new errors.IncorrectUsageError({
20
- message: i18n.t('warnings.helpers.plural.valuesMustBeDefined')
26
+ message: tpl(messages.valuesMustBeDefined)
21
27
  });
22
28
  }
23
29
 
@@ -2,7 +2,7 @@
2
2
  // Usage: `{{post_class}}`
3
3
  //
4
4
  // Output classes for the body element
5
- const {SafeString} = require('../services/proxy');
5
+ const {SafeString} = require('../services/rendering');
6
6
 
7
7
  // We use the name post_class to match the helper for consistency:
8
8
  module.exports = function post_class() { // eslint-disable-line camelcase
@@ -2,12 +2,19 @@
2
2
  // Example usages
3
3
  // `{{#prev_post}}<a href ="{{url}}>previous post</a>{{/prev_post}}'
4
4
  // `{{#next_post}}<a href ="{{url absolute="true">next post</a>{{/next_post}}'
5
+ const {api, checks} = require('../services/proxy');
6
+ const {hbs} = require('../services/rendering');
5
7
 
6
- const {logging, i18n, api, hbs, checks} = require('../services/proxy');
8
+ const logging = require('@tryghost/logging');
9
+ const tpl = require('@tryghost/tpl');
7
10
  const get = require('lodash/get');
8
11
  const Promise = require('bluebird');
9
12
  const moment = require('moment');
10
13
 
14
+ const messages = {
15
+ mustBeCalledAsBlock: 'The {{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}'
16
+ };
17
+
11
18
  const createFrame = hbs.handlebars.createFrame;
12
19
 
13
20
  const buildApiOptions = function buildApiOptions(options, post) {
@@ -78,7 +85,7 @@ module.exports = function prevNext(options) {
78
85
 
79
86
  // Guard against incorrect usage of the helpers
80
87
  if (!options.fn || !options.inverse) {
81
- data.error = i18n.t('warnings.helpers.mustBeCalledAsBlock', {helperName: options.name});
88
+ data.error = tpl(messages.mustBeCalledAsBlock, {helperName: options.name});
82
89
  logging.warn(data.error);
83
90
  return Promise.resolve();
84
91
  }
@@ -10,10 +10,15 @@
10
10
  //
11
11
  // Returns amount equal to the dominant denomintation of the currency.
12
12
  // For example, if 2100 is passed, it will return 21.
13
- const isNumber = require('lodash/isNumber');
14
- const {errors, i18n} = require('../services/proxy');
13
+ const errors = require('@tryghost/errors');
14
+ const tpl = require('@tryghost/tpl');
15
15
  const _ = require('lodash');
16
16
 
17
+ const messages = {
18
+ attrIsRequired: 'Attribute is required e.g. {{price plan.amount}}',
19
+ attrMustBeNumeric: 'Attribute value should be a number'
20
+ };
21
+
17
22
  function formatter({amount, currency, numberFormat = 'short', currencyFormat = 'symbol', locale}) {
18
23
  const formatterOptions = {
19
24
  style: 'currency',
@@ -77,20 +82,20 @@ module.exports = function price(planOrAmount, options) {
77
82
  // CASE: if no amount is passed, e.g. `{{price}}` we throw an error
78
83
  if (arguments.length < 2) {
79
84
  throw new errors.IncorrectUsageError({
80
- message: i18n.t('warnings.helpers.price.attrIsRequired')
85
+ message: tpl(messages.attrIsRequired)
81
86
  });
82
87
  }
83
88
 
84
89
  // CASE: if amount is passed, but it is undefined we throw an error
85
90
  if (amount === undefined) {
86
91
  throw new errors.IncorrectUsageError({
87
- message: i18n.t('warnings.helpers.price.attrIsRequired')
92
+ message: tpl(messages.attrIsRequired)
88
93
  });
89
94
  }
90
95
 
91
- if (!isNumber(amount)) {
96
+ if (!_.isNumber(amount)) {
92
97
  throw new errors.IncorrectUsageError({
93
- message: i18n.t('warnings.helpers.price.attrMustBeNumeric')
98
+ message: tpl(messages.attrMustBeNumeric)
94
99
  });
95
100
  }
96
101
 
@@ -3,10 +3,11 @@
3
3
  //
4
4
  // Returns a string of the products with access to the post.
5
5
  // By default, products are separated by commas.
6
+ const {labs} = require('../services/proxy');
7
+ const {SafeString} = require('../services/rendering');
6
8
 
7
9
  const nql = require('@nexes/nql');
8
10
  const isString = require('lodash/isString');
9
- const {SafeString, labs} = require('../services/proxy');
10
11
 
11
12
  function products(options = {}) {
12
13
  options = options || {};
@@ -10,8 +10,10 @@
10
10
  //
11
11
  // Returns estimated reading time for post
12
12
 
13
- const {SafeString, checks} = require('../services/proxy');
14
- const calculateReadingTime = require('@tryghost/helpers').readingTime;
13
+ const {checks} = require('../services/proxy');
14
+ const {SafeString} = require('../services/rendering');
15
+
16
+ const {readingTime: calculateReadingTime} = require('@tryghost/helpers');
15
17
 
16
18
  module.exports = function reading_time(options) {// eslint-disable-line camelcase
17
19
  options = options || {};
@@ -10,7 +10,7 @@
10
10
  // because often other helpers need that (t) returns a string to be able to work as subexpression; e.g.:
11
11
  // {{tags prefix=(t " on ")}}
12
12
 
13
- const {themeI18n} = require('../services/proxy');
13
+ const {themeI18n} = require('../services/rendering');
14
14
 
15
15
  module.exports = function t(text, options) {
16
16
  const bindings = {};
@@ -5,7 +5,9 @@
5
5
  // By default, tags are separated by commas.
6
6
  //
7
7
  // Note that the standard {{#each tags}} implementation is unaffected by this helper
8
- const {urlService, SafeString, escapeExpression, templates} = require('../services/proxy');
8
+ const {urlService} = require('../services/proxy');
9
+ const {SafeString, escapeExpression, templates} = require('../services/rendering');
10
+
9
11
  const isString = require('lodash/isString');
10
12
  const ghostHelperUtils = require('@tryghost/helpers').utils;
11
13
 
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // Overrides the standard behaviour of `{[title}}` to ensure the content is correctly escaped
5
5
 
6
- const {SafeString, escapeExpression} = require('../services/proxy');
6
+ const {SafeString, escapeExpression} = require('../services/rendering');
7
7
 
8
8
  module.exports = function title() {
9
9
  return new SafeString(escapeExpression(this.title || ''));
@@ -2,7 +2,8 @@
2
2
  // Usage: `{{twitter_url}}` or `{{twitter_url author.twitter}}`
3
3
  //
4
4
  // Output a url for a twitter username
5
- const {socialUrls, localUtils} = require('../services/proxy');
5
+ const {socialUrls} = require('../services/proxy');
6
+ const {localUtils} = require('../services/rendering');
6
7
 
7
8
  // We use the name twitter_url to match the helper for consistency:
8
9
  module.exports = function twitter_url(username, options) { // eslint-disable-line camelcase
@@ -4,7 +4,9 @@
4
4
  // Returns the URL for the current object scope i.e. If inside a post scope will return post permalink
5
5
  // `absolute` flag outputs absolute URL, else URL is relative
6
6
 
7
- const {SafeString, metaData} = require('../services/proxy');
7
+ const {metaData} = require('../services/proxy');
8
+ const {SafeString} = require('../services/rendering');
9
+
8
10
  const {getMetaDataUrl} = metaData;
9
11
 
10
12
  module.exports = function url(options) {
@@ -1,74 +1,51 @@
1
- // This file defines everything that helpers "require"
2
- // With the exception of modules like lodash, Bluebird
3
- // We can later refactor to enforce this something like we did in apps
4
- const hbs = require('./theme-engine/engine');
5
- const errors = require('@tryghost/errors');
6
-
7
- const i18n = require('../../shared/i18n');
8
- const logging = require('@tryghost/logging');
1
+ // This file contains everything that the helpers and frontend apps require from the core of Ghost
9
2
  const settingsCache = require('../../shared/settings-cache');
10
3
  const config = require('../../shared/config');
11
4
 
12
- // Direct requires:
13
- // - lodash
14
- // - bluebird
15
- // - downsize
16
- // - moment-timezone
17
- // - jsonpath
5
+ // Require from the rendering framework
6
+ const {SafeString} = require('./rendering');
18
7
 
19
8
  module.exports = {
20
- hbs: hbs,
21
- SafeString: hbs.SafeString,
22
- escapeExpression: hbs.escapeExpression,
23
-
24
- // TODO: Expose less of the API to make this safe
25
- api: require('../../server/api'),
26
-
27
- // TODO: Only expose "get"
28
- settingsCache: settingsCache,
29
-
30
- // These 3 are kind of core and required all the time
31
- errors,
32
- i18n,
33
- logging,
34
-
35
- // Theme i18n is separate to common i18n
36
- themeI18n: require('./theme-engine/i18n'),
37
-
38
- // This is used to detect if "isPost" is true in prevNext.
9
+ /**
10
+ * Section two: data manipulation
11
+ * Stuff that modifies API data (SDK layer)
12
+ */
13
+ metaData: require('../meta'),
14
+ socialUrls: require('@tryghost/social-urls'),
15
+ blogIcon: require('../../server/lib/image').blogIcon,
16
+ // Used by router service and {{get}} helper to prepare data for optimal usage in themes
17
+ prepareContextResource(data) {
18
+ (Array.isArray(data) ? data : [data]).forEach((resource) => {
19
+ // feature_image_caption contains HTML, making it a SafeString spares theme devs from triple-curlies
20
+ if (resource.feature_image_caption) {
21
+ resource.feature_image_caption = new SafeString(resource.feature_image_caption);
22
+ }
23
+ });
24
+ },
25
+ // This is used to decide e.g. if a JSON object is a Post, Page, Tag etc
39
26
  checks: require('../../server/data/schema').checks,
40
27
 
41
- // Config!
42
- // Keys used:
28
+ /**
29
+ * Section three: Core API
30
+ * Parts of Ghost core that the frontend currently needs
31
+ */
32
+
33
+ // Config! Keys used:
43
34
  // isPrivacyDisabled & referrerPolicy used in ghost_head
44
35
  config: {
45
36
  get: config.get.bind(config),
46
37
  isPrivacyDisabled: config.isPrivacyDisabled.bind(config)
47
38
  },
48
39
 
49
- // Labs utils for enabling/disabling helpers
50
- labs: require('../../shared/labs'),
51
-
52
- // Things required from data/meta
53
- metaData: require('../meta'),
40
+ // TODO: Only expose "get"
41
+ settingsCache: settingsCache,
54
42
 
55
- // The local template thing, should this be merged with the channels one?
56
- templates: require('./theme-engine/handlebars/template'),
43
+ // TODO: Expose less of the API to make this safe
44
+ api: require('../../server/api'),
57
45
 
58
- // Various utils, needs cleaning up / simplifying
59
- socialUrls: require('@tryghost/social-urls'),
60
- blogIcon: require('../../server/lib/image').blogIcon,
46
+ // Labs utils for enabling/disabling helpers
47
+ labs: require('../../shared/labs'),
48
+ // URGH... Yuk (unhelpful comment :D)
61
49
  urlService: require('./url'),
62
- urlUtils: require('../../shared/url-utils'),
63
- localUtils: require('./theme-engine/handlebars/utils'),
64
-
65
- // Used by router service and {{get}} helper to prepare data for optimal usage in themes
66
- prepareContextResource(data) {
67
- (Array.isArray(data) ? data : [data]).forEach((resource) => {
68
- // feature_image_caption contains HTML, making it a SafeString spares theme devs from triple-curlies
69
- if (resource.feature_image_caption) {
70
- resource.feature_image_caption = new hbs.SafeString(resource.feature_image_caption);
71
- }
72
- });
73
- }
50
+ urlUtils: require('../../shared/url-utils')
74
51
  };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This is a loose concept of a frontend rendering framework
3
+ * Note: everything here gets deep-required from the theme-engine
4
+ * This indicates that the theme engine is a set of services, rather than a single service
5
+ * and could do with a refactor.
6
+ *
7
+ * This at least keeps the deep requires in a single place.
8
+ */
9
+
10
+ const hbs = require('./theme-engine/engine');
11
+
12
+ module.exports = {
13
+ hbs: hbs,
14
+ SafeString: hbs.SafeString,
15
+ escapeExpression: hbs.escapeExpression,
16
+ // The local template thing, should this be merged with the channels one?
17
+ templates: require('./theme-engine/handlebars/template'),
18
+
19
+ // Theme i18n is separate to common i18n
20
+ themeI18n: require('./theme-engine/i18n'),
21
+
22
+ // TODO: these need a more sensible home
23
+ localUtils: require('./theme-engine/handlebars/utils')
24
+ };
@@ -1,11 +1,15 @@
1
1
  const _ = require('lodash');
2
2
  const debug = require('@tryghost/debug')('services:routing:controllers:channel');
3
- const i18n = require('../../../../shared/i18n');
3
+ const tpl = require('@tryghost/tpl');
4
4
  const errors = require('@tryghost/errors');
5
5
  const security = require('@tryghost/security');
6
6
  const themeEngine = require('../../theme-engine');
7
7
  const helpers = require('../helpers');
8
8
 
9
+ const messages = {
10
+ pageNotFound: 'Page not found.'
11
+ };
12
+
9
13
  /**
10
14
  * @description Channel controller.
11
15
  *
@@ -51,7 +55,7 @@ module.exports = function channelController(req, res, next) {
51
55
  // CASE: requested page is greater than number of pages we have
52
56
  if (pathOptions.page > result.meta.pagination.pages) {
53
57
  return next(new errors.NotFoundError({
54
- message: i18n.t('errors.errors.pageNotFound')
58
+ message: tpl(messages.pageNotFound)
55
59
  }));
56
60
  }
57
61
 
@@ -1,12 +1,16 @@
1
1
  const _ = require('lodash');
2
2
  const debug = require('@tryghost/debug')('services:routing:controllers:collection');
3
- const {i18n} = require('../../proxy');
3
+ const tpl = require('@tryghost/tpl');
4
4
  const errors = require('@tryghost/errors');
5
5
  const security = require('@tryghost/security');
6
6
  const urlService = require('../../url');
7
7
  const themeEngine = require('../../theme-engine');
8
8
  const helpers = require('../helpers');
9
9
 
10
+ const messages = {
11
+ pageNotFound: 'Page not found.'
12
+ };
13
+
10
14
  /**
11
15
  * @description Collection controller.
12
16
  * @param {Object} req
@@ -50,7 +54,7 @@ module.exports = function collectionController(req, res, next) {
50
54
  // CASE: requested page is greater than number of pages we have
51
55
  if (pathOptions.page > result.meta.pagination.pages) {
52
56
  return next(new errors.NotFoundError({
53
- message: i18n.t('errors.errors.pageNotFound')
57
+ message: tpl(messages.pageNotFound)
54
58
  }));
55
59
  }
56
60
 
@@ -1,7 +1,11 @@
1
- const i18n = require('../../../../shared/i18n');
1
+ const tpl = require('@tryghost/tpl');
2
2
  const errors = require('@tryghost/errors');
3
3
  const urlUtils = require('../../../../shared/url-utils');
4
4
 
5
+ const messages = {
6
+ pageNotFound: 'Page not found.'
7
+ };
8
+
5
9
  /**
6
10
  * @description Middleware, which validates and interprets the page param e.g. /page/1
7
11
  * @param {Object} req
@@ -21,7 +25,7 @@ module.exports = function handlePageParam(req, res, next, page) {
21
25
  return urlUtils.redirect301(res, req.originalUrl.replace(pageRegex, '/'));
22
26
  } else if (page < 1 || isNaN(page)) {
23
27
  return next(new errors.NotFoundError({
24
- message: i18n.t('errors.errors.pageNotFound')
28
+ message: tpl(messages.pageNotFound)
25
29
  }));
26
30
  } else {
27
31
  req.params.page = page;
@@ -5,6 +5,7 @@ const {api} = require('../proxy');
5
5
  const errors = require('@tryghost/errors');
6
6
  const tpl = require('@tryghost/tpl');
7
7
  const settingsCache = require('../../../shared/settings-cache');
8
+ const customThemeSettingsCache = require('../../../shared/custom-theme-settings-cache');
8
9
  const labs = require('../../../shared/labs');
9
10
  const activeTheme = require('./active');
10
11
  const preview = require('./preview');
@@ -86,12 +87,9 @@ async function getProductAndPricesData() {
86
87
  }
87
88
  }
88
89
 
89
- function getSiteData(req) {
90
+ function getSiteData() {
90
91
  let siteData = settingsCache.getPublic();
91
92
 
92
- // @TODO: it would be nicer if this was proper middleware somehow...
93
- siteData = preview.handle(req, siteData);
94
-
95
93
  // theme-only computed property added to @site
96
94
  if (settingsCache.get('members_signup_access') === 'none') {
97
95
  const escapedUrl = encodeURIComponent(urlUtils.urlFor({relativeUrl: '/rss/'}, true));
@@ -107,13 +105,14 @@ async function updateGlobalTemplateOptions(req, res, next) {
107
105
  // Static information, same for every request unless the settings change
108
106
  // @TODO: bind this once and then update based on events?
109
107
  // @TODO: decouple theme layer from settings cache using the Content API
110
- const siteData = getSiteData(req);
108
+ const siteData = getSiteData();
111
109
  const labsData = labs.getAll();
112
110
 
113
111
  const themeData = {
114
112
  posts_per_page: activeTheme.get().config('posts_per_page'),
115
113
  image_sizes: activeTheme.get().config('image_sizes')
116
114
  };
115
+ const themeSettingsData = customThemeSettingsCache.getAll();
117
116
  const productData = await getProductAndPricesData();
118
117
  const priceData = calculateLegacyPriceData(productData);
119
118
 
@@ -136,7 +135,8 @@ async function updateGlobalTemplateOptions(req, res, next) {
136
135
  config: themeData,
137
136
  price: priceData,
138
137
  product,
139
- products
138
+ products,
139
+ custom: themeSettingsData
140
140
  }
141
141
  });
142
142
  }
@@ -154,10 +154,26 @@ function updateLocalTemplateData(req, res, next) {
154
154
 
155
155
  function updateLocalTemplateOptions(req, res, next) {
156
156
  const localTemplateOptions = hbs.getLocalTemplateOptions(res.locals);
157
+
158
+ // adjust @site.url for http/https based on the incoming request
157
159
  const siteData = {
158
160
  url: urlUtils.urlFor('home', {secure: req.secure, trailingSlash: false}, true)
159
161
  };
160
162
 
163
+ // @TODO: it would be nicer if this was proper middleware somehow...
164
+ const previewData = preview.handle(req, Object.keys(customThemeSettingsCache.getAll()));
165
+
166
+ // strip custom off of preview data so it doesn't get merged into @site
167
+ const customThemeSettingsPreviewData = previewData.custom;
168
+ delete previewData.custom;
169
+ let customData = {};
170
+ if (labs.isSet('customThemeSettings')) {
171
+ customData = customThemeSettingsPreviewData;
172
+ }
173
+
174
+ // update site data with any preview values from the request
175
+ Object.assign(siteData, previewData);
176
+
161
177
  const member = req.member ? {
162
178
  uuid: req.member.uuid,
163
179
  email: req.member.email,
@@ -176,6 +192,7 @@ function updateLocalTemplateOptions(req, res, next) {
176
192
  data: {
177
193
  member: member,
178
194
  site: siteData,
195
+ custom: customData,
179
196
  // @deprecated: a gscan warning for @blog was added before 3.0 which replaced it with @site
180
197
  blog: siteData
181
198
  }
@@ -14,34 +14,57 @@ function decodeValue(value) {
14
14
  return value;
15
15
  }
16
16
 
17
- function getPreviewData(previewHeader, siteData) {
17
+ function getPreviewData(previewHeader, customThemeSettingKeys = []) {
18
18
  // Keep the string shorter with short codes for certain parameters
19
19
  const supportedSettings = {
20
20
  c: 'accent_color',
21
21
  icon: 'icon',
22
22
  logo: 'logo',
23
- cover: 'cover_image'
23
+ cover: 'cover_image',
24
+ custom: 'custom',
25
+ d: 'description'
24
26
  };
25
27
 
26
28
  let opts = new URLSearchParams(previewHeader);
27
29
 
30
+ const previewData = {};
31
+
28
32
  opts.forEach((value, key) => {
29
33
  value = decodeValue(value);
30
34
  if (supportedSettings[key]) {
31
- _.set(siteData, supportedSettings[key], value);
35
+ _.set(previewData, supportedSettings[key], value);
32
36
  }
33
37
  });
34
38
 
35
- siteData._preview = previewHeader;
39
+ if (previewData.custom) {
40
+ try {
41
+ const custom = {};
42
+ const previewCustom = JSON.parse(previewData.custom);
43
+
44
+ if (typeof previewCustom === 'object') {
45
+ customThemeSettingKeys.forEach((key) => {
46
+ custom[key] = previewCustom[key];
47
+ });
48
+ }
36
49
 
37
- return siteData;
50
+ previewData.custom = custom;
51
+ } catch (e) {
52
+ previewData.custom = {};
53
+ }
54
+ }
55
+
56
+ previewData._preview = previewHeader;
57
+
58
+ return previewData;
38
59
  }
39
60
 
40
61
  module.exports._PREVIEW_HEADER_NAME = PREVIEW_HEADER_NAME;
41
- module.exports.handle = (req, siteData) => {
62
+ module.exports.handle = (req, customThemeSettingKeys) => {
63
+ let previewData = {};
64
+
42
65
  if (req && req.header(PREVIEW_HEADER_NAME)) {
43
- siteData = getPreviewData(req.header(PREVIEW_HEADER_NAME), siteData);
66
+ previewData = getPreviewData(req.header(PREVIEW_HEADER_NAME), customThemeSettingKeys);
44
67
  }
45
68
 
46
- return siteData;
69
+ return previewData;
47
70
  };