ghost 5.26.4 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/components/tryghost-adapter-manager-5.28.0.tgz +0 -0
  2. package/components/{tryghost-api-framework-5.26.4.tgz → tryghost-api-framework-5.28.0.tgz} +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-5.26.4.tgz → tryghost-api-version-compatibility-service-5.28.0.tgz} +0 -0
  4. package/components/tryghost-audience-feedback-5.28.0.tgz +0 -0
  5. package/components/tryghost-bootstrap-socket-5.28.0.tgz +0 -0
  6. package/components/tryghost-constants-5.28.0.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.26.4.tgz → tryghost-custom-theme-settings-service-5.28.0.tgz} +0 -0
  8. package/components/tryghost-data-generator-5.28.0.tgz +0 -0
  9. package/components/tryghost-domain-events-5.28.0.tgz +0 -0
  10. package/components/tryghost-email-analytics-provider-mailgun-5.28.0.tgz +0 -0
  11. package/components/tryghost-email-analytics-service-5.28.0.tgz +0 -0
  12. package/components/tryghost-email-content-generator-5.28.0.tgz +0 -0
  13. package/components/tryghost-email-events-5.28.0.tgz +0 -0
  14. package/components/tryghost-email-service-5.28.0.tgz +0 -0
  15. package/components/tryghost-email-suppression-list-5.28.0.tgz +0 -0
  16. package/components/tryghost-express-dynamic-redirects-5.28.0.tgz +0 -0
  17. package/components/tryghost-extract-api-key-5.28.0.tgz +0 -0
  18. package/components/tryghost-html-to-plaintext-5.28.0.tgz +0 -0
  19. package/components/tryghost-importer-revue-5.28.0.tgz +0 -0
  20. package/components/tryghost-job-manager-5.28.0.tgz +0 -0
  21. package/components/tryghost-link-redirects-5.28.0.tgz +0 -0
  22. package/components/tryghost-link-replacer-5.28.0.tgz +0 -0
  23. package/components/tryghost-link-tracking-5.28.0.tgz +0 -0
  24. package/components/tryghost-magic-link-5.28.0.tgz +0 -0
  25. package/components/tryghost-mailgun-client-5.28.0.tgz +0 -0
  26. package/components/{tryghost-member-attribution-5.26.4.tgz → tryghost-member-attribution-5.28.0.tgz} +0 -0
  27. package/components/{tryghost-member-events-5.26.4.tgz → tryghost-member-events-5.28.0.tgz} +0 -0
  28. package/components/{tryghost-members-api-5.26.4.tgz → tryghost-members-api-5.28.0.tgz} +0 -0
  29. package/components/tryghost-members-csv-5.28.0.tgz +0 -0
  30. package/components/tryghost-members-events-service-5.28.0.tgz +0 -0
  31. package/components/tryghost-members-importer-5.28.0.tgz +0 -0
  32. package/components/tryghost-members-offers-5.28.0.tgz +0 -0
  33. package/components/tryghost-members-payments-5.28.0.tgz +0 -0
  34. package/components/tryghost-members-ssr-5.28.0.tgz +0 -0
  35. package/components/tryghost-members-stripe-service-5.28.0.tgz +0 -0
  36. package/components/tryghost-minifier-5.28.0.tgz +0 -0
  37. package/components/tryghost-mw-api-version-mismatch-5.28.0.tgz +0 -0
  38. package/components/tryghost-mw-cache-control-5.28.0.tgz +0 -0
  39. package/components/tryghost-mw-error-handler-5.28.0.tgz +0 -0
  40. package/components/tryghost-mw-session-from-token-5.28.0.tgz +0 -0
  41. package/components/tryghost-mw-update-user-last-seen-5.28.0.tgz +0 -0
  42. package/components/tryghost-mw-vhost-5.28.0.tgz +0 -0
  43. package/components/tryghost-oembed-service-5.28.0.tgz +0 -0
  44. package/components/tryghost-package-json-5.28.0.tgz +0 -0
  45. package/components/tryghost-referrers-5.28.0.tgz +0 -0
  46. package/components/tryghost-security-5.28.0.tgz +0 -0
  47. package/components/tryghost-session-service-5.28.0.tgz +0 -0
  48. package/components/tryghost-settings-path-manager-5.28.0.tgz +0 -0
  49. package/components/{tryghost-staff-service-5.26.4.tgz → tryghost-staff-service-5.28.0.tgz} +0 -0
  50. package/components/tryghost-stats-service-5.28.0.tgz +0 -0
  51. package/components/tryghost-tiers-5.28.0.tgz +0 -0
  52. package/components/tryghost-update-check-service-5.28.0.tgz +0 -0
  53. package/components/tryghost-verification-trigger-5.28.0.tgz +0 -0
  54. package/components/tryghost-version-notifications-data-service-5.28.0.tgz +0 -0
  55. package/core/built/admin/assets/{chunk.143.f9454a947ce3ca9468a9.js → chunk.143.16c07be45a0c39262995.js} +6 -6
  56. package/core/built/admin/assets/{chunk.178.5730ec1241c554356843.js → chunk.178.93714202b2fc31b2b86a.js} +4 -4
  57. package/core/built/admin/assets/{chunk.613.25718fa355a25fc2a7c1.js → chunk.380.de3a9bf5161ab5300d23.js} +4790 -4306
  58. package/core/built/admin/assets/{chunk.613.25718fa355a25fc2a7c1.js.LICENSE.txt → chunk.380.de3a9bf5161ab5300d23.js.LICENSE.txt} +0 -0
  59. package/core/built/admin/assets/ghost-1532517fd24b662b42a85ea0881865a5.css +1 -0
  60. package/core/built/admin/assets/{ghost-5429d972966c85a0a24d766f93db999d.js → ghost-d8834e309d8e4800057d6d17fed9415b.js} +4311 -4289
  61. package/core/built/admin/assets/ghost-dark-3b8aff3cf9db67f6ad59abec25ae98be.css +1 -0
  62. package/core/built/admin/assets/{vendor-5aae14724f891e36c43786254980485c.js → vendor-dbf84caa0968ef5da0486055da6636da.js} +113 -63
  63. package/core/built/admin/index.html +6 -6
  64. package/core/frontend/helpers/get.js +2 -2
  65. package/core/frontend/web/middleware/error-handler.js +0 -2
  66. package/core/server/api/endpoints/themes.js +13 -9
  67. package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +2 -0
  68. package/core/server/data/migrations/versions/5.27/2022-12-13-16-15-add-usage-colums-to-tokens.js +20 -0
  69. package/core/server/data/migrations/versions/5.27/2023-01-04-04-12-drop-suppressions-table.js +14 -0
  70. package/core/server/data/migrations/versions/5.27/2023-01-04-04-13-add-suppressions-table.js +9 -0
  71. package/core/server/data/migrations/versions/5.28/2023-01-05-15-13-add-active-theme-permissions.js +12 -0
  72. package/core/server/data/schema/fixtures/fixtures.json +7 -2
  73. package/core/server/data/schema/schema.js +4 -1
  74. package/core/server/models/member.js +2 -1
  75. package/core/server/models/product.js +1 -1
  76. package/core/server/models/single-use-token.js +2 -25
  77. package/core/server/services/bulk-email/bulk-email-processor.js +23 -4
  78. package/core/server/services/email-service/wrapper.js +2 -1
  79. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +13 -5
  80. package/core/server/services/mega/mega.js +9 -1
  81. package/core/server/services/members/SingleUseTokenProvider.js +51 -7
  82. package/core/server/services/members/api.js +8 -1
  83. package/core/server/services/members/jobs/clean-tokens.js +50 -0
  84. package/core/server/services/members/jobs/index.js +29 -4
  85. package/core/server/services/members/service.js +12 -19
  86. package/core/server/services/newsletters/index.js +8 -1
  87. package/core/server/services/settings/settings-service.js +8 -1
  88. package/core/server/services/themes/activate.js +3 -3
  89. package/core/server/services/themes/index.js +4 -2
  90. package/core/server/services/themes/storage.js +3 -1
  91. package/core/server/services/themes/to-json.js +6 -6
  92. package/core/server/services/themes/validate.js +80 -8
  93. package/core/server/web/api/endpoints/admin/routes.js +5 -0
  94. package/core/shared/config/defaults.json +3 -2
  95. package/core/shared/labs.js +2 -1
  96. package/package.json +133 -133
  97. package/yarn.lock +1840 -2000
  98. package/components/tryghost-adapter-manager-5.26.4.tgz +0 -0
  99. package/components/tryghost-audience-feedback-5.26.4.tgz +0 -0
  100. package/components/tryghost-bootstrap-socket-5.26.4.tgz +0 -0
  101. package/components/tryghost-constants-5.26.4.tgz +0 -0
  102. package/components/tryghost-data-generator-5.26.4.tgz +0 -0
  103. package/components/tryghost-domain-events-5.26.4.tgz +0 -0
  104. package/components/tryghost-email-analytics-provider-mailgun-5.26.4.tgz +0 -0
  105. package/components/tryghost-email-analytics-service-5.26.4.tgz +0 -0
  106. package/components/tryghost-email-content-generator-5.26.4.tgz +0 -0
  107. package/components/tryghost-email-events-5.26.4.tgz +0 -0
  108. package/components/tryghost-email-service-5.26.4.tgz +0 -0
  109. package/components/tryghost-email-suppression-list-5.26.4.tgz +0 -0
  110. package/components/tryghost-express-dynamic-redirects-5.26.4.tgz +0 -0
  111. package/components/tryghost-extract-api-key-5.26.4.tgz +0 -0
  112. package/components/tryghost-html-to-plaintext-5.26.4.tgz +0 -0
  113. package/components/tryghost-importer-revue-5.26.4.tgz +0 -0
  114. package/components/tryghost-job-manager-5.26.4.tgz +0 -0
  115. package/components/tryghost-link-redirects-5.26.4.tgz +0 -0
  116. package/components/tryghost-link-replacer-5.26.4.tgz +0 -0
  117. package/components/tryghost-link-tracking-5.26.4.tgz +0 -0
  118. package/components/tryghost-magic-link-5.26.4.tgz +0 -0
  119. package/components/tryghost-mailgun-client-5.26.4.tgz +0 -0
  120. package/components/tryghost-members-csv-5.26.4.tgz +0 -0
  121. package/components/tryghost-members-events-service-5.26.4.tgz +0 -0
  122. package/components/tryghost-members-importer-5.26.4.tgz +0 -0
  123. package/components/tryghost-members-offers-5.26.4.tgz +0 -0
  124. package/components/tryghost-members-payments-5.26.4.tgz +0 -0
  125. package/components/tryghost-members-ssr-5.26.4.tgz +0 -0
  126. package/components/tryghost-members-stripe-service-5.26.4.tgz +0 -0
  127. package/components/tryghost-minifier-5.26.4.tgz +0 -0
  128. package/components/tryghost-mw-api-version-mismatch-5.26.4.tgz +0 -0
  129. package/components/tryghost-mw-cache-control-5.26.4.tgz +0 -0
  130. package/components/tryghost-mw-error-handler-5.26.4.tgz +0 -0
  131. package/components/tryghost-mw-session-from-token-5.26.4.tgz +0 -0
  132. package/components/tryghost-mw-update-user-last-seen-5.26.4.tgz +0 -0
  133. package/components/tryghost-mw-vhost-5.26.4.tgz +0 -0
  134. package/components/tryghost-oembed-service-5.26.4.tgz +0 -0
  135. package/components/tryghost-package-json-5.26.4.tgz +0 -0
  136. package/components/tryghost-referrers-5.26.4.tgz +0 -0
  137. package/components/tryghost-security-5.26.4.tgz +0 -0
  138. package/components/tryghost-session-service-5.26.4.tgz +0 -0
  139. package/components/tryghost-settings-path-manager-5.26.4.tgz +0 -0
  140. package/components/tryghost-stats-service-5.26.4.tgz +0 -0
  141. package/components/tryghost-tiers-5.26.4.tgz +0 -0
  142. package/components/tryghost-update-check-service-5.26.4.tgz +0 -0
  143. package/components/tryghost-verification-trigger-5.26.4.tgz +0 -0
  144. package/components/tryghost-version-notifications-data-service-5.26.4.tgz +0 -0
  145. package/core/built/admin/assets/ghost-830670d910cea64d98ab083aafd1dffd.css +0 -1
  146. package/core/built/admin/assets/ghost-dark-e9195020925a360d83163e1e87b514a6.css +0 -1
@@ -67,11 +67,11 @@ const processImport = async (options) => {
67
67
  return await membersImporter.process({...options, verificationTrigger});
68
68
  };
69
69
 
70
- const updateVerificationTrigger = () => {
71
- verificationTrigger = new VerificationTrigger({
72
- apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.apiThreshold'),
73
- adminTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
74
- importTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
70
+ const initVerificationTrigger = () => {
71
+ return new VerificationTrigger({
72
+ getApiTriggerThreshold: () => _.get(config.get('hostSettings'), 'emailVerification.apiThreshold'),
73
+ getAdminTriggerThreshold: () => _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
74
+ getImportTriggerThreshold: () => _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
75
75
  isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
76
76
  isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
77
77
  sendVerificationEmail: async ({subject, message, amountTriggered}) => {
@@ -133,16 +133,8 @@ module.exports = {
133
133
  getMembersApi: () => module.exports.api
134
134
  });
135
135
 
136
- updateVerificationTrigger();
137
-
138
- (async () => {
139
- try {
140
- const collection = await models.SingleUseToken.fetchAll();
141
- await collection.invokeThen('destroy');
142
- } catch (err) {
143
- logging.error(err);
144
- }
145
- })();
136
+ verificationTrigger = initVerificationTrigger();
137
+ module.exports.verificationTrigger = verificationTrigger;
146
138
 
147
139
  if (!env?.startsWith('testing')) {
148
140
  const membersMigrationJobName = 'members-migrations';
@@ -159,6 +151,9 @@ module.exports = {
159
151
 
160
152
  // Schedule daily cron job to clean expired comp subs
161
153
  memberJobs.scheduleExpiredCompCleanupJob();
154
+
155
+ // Schedule daily cron job to clean expired tokens
156
+ memberJobs.scheduleTokenCleanupJob();
162
157
  },
163
158
  contentGating: require('./content-gating'),
164
159
 
@@ -169,16 +164,14 @@ module.exports = {
169
164
  },
170
165
 
171
166
  ssr: null,
167
+ verificationTrigger: null,
172
168
 
173
169
  stripeConnect: require('./stripe-connect'),
174
170
 
175
171
  processImport: processImport,
176
172
 
177
173
  stats: membersStats,
178
- export: require('./exporter/query'),
179
-
180
- // Only for tests
181
- _updateVerificationTrigger: updateVerificationTrigger
174
+ export: require('./exporter/query')
182
175
  };
183
176
 
184
177
  module.exports.middleware = require('./middleware');
@@ -7,12 +7,19 @@ const limitService = require('../limits');
7
7
  const labs = require('../../../shared/labs');
8
8
 
9
9
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
10
+ const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
11
+ const MAGIC_LINK_TOKEN_MAX_USAGE_COUNT = 3;
10
12
 
11
13
  module.exports = new NewslettersService({
12
14
  NewsletterModel: models.Newsletter,
13
15
  MemberModel: models.Member,
14
16
  mail,
15
- singleUseTokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY),
17
+ singleUseTokenProvider: new SingleUseTokenProvider({
18
+ SingleUseTokenModel: models.SingleUseToken,
19
+ validityPeriod: MAGIC_LINK_TOKEN_VALIDITY,
20
+ validityPeriodAfterUsage: MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE,
21
+ maxUsageCount: MAGIC_LINK_TOKEN_MAX_USAGE_COUNT
22
+ }),
16
23
  urlUtils,
17
24
  limitService,
18
25
  labs
@@ -17,6 +17,8 @@ const ObjectId = require('bson-objectid').default;
17
17
  const settingsHelpers = require('../settings-helpers');
18
18
 
19
19
  const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
20
+ const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
21
+ const MAGIC_LINK_TOKEN_MAX_USAGE_COUNT = 3;
20
22
 
21
23
  /**
22
24
  * @returns {SettingsBREADService} instance of the PostsService
@@ -27,7 +29,12 @@ const getSettingsBREADServiceInstance = () => {
27
29
  settingsCache: SettingsCache,
28
30
  labsService: labs,
29
31
  mail,
30
- singleUseTokenProvider: new SingleUseTokenProvider(models.SingleUseToken, MAGIC_LINK_TOKEN_VALIDITY),
32
+ singleUseTokenProvider: new SingleUseTokenProvider({
33
+ SingleUseTokenModel: models.SingleUseToken,
34
+ validityPeriod: MAGIC_LINK_TOKEN_VALIDITY,
35
+ validityPeriodAfterUsage: MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE,
36
+ maxUsageCount: MAGIC_LINK_TOKEN_MAX_USAGE_COUNT
37
+ }),
31
38
  urlUtils
32
39
  });
33
40
  };
@@ -21,7 +21,7 @@ module.exports.loadAndActivate = async (themeName) => {
21
21
  const loadedTheme = await themeLoader.loadOneTheme(themeName);
22
22
  // Validate
23
23
  // @NOTE: this is now the only usage of check, rather than checkSafe...
24
- const checkedTheme = await validate.check(loadedTheme);
24
+ const checkedTheme = await validate.check(themeName, loadedTheme);
25
25
 
26
26
  if (!validate.canActivate(checkedTheme)) {
27
27
  logging.error(validate.getThemeValidationError('activeThemeHasFatalErrors', themeName, checkedTheme));
@@ -57,6 +57,6 @@ module.exports.activate = async (themeName) => {
57
57
  const checkedTheme = await validate.checkSafe(themeName, loadedTheme);
58
58
  // Activate
59
59
  await activator.activateFromAPI(themeName, loadedTheme, checkedTheme);
60
- // Return the checked theme
61
- return checkedTheme;
60
+ // Return the theme errors
61
+ return validate.getErrorsFromCheckedTheme(checkedTheme);
62
62
  };
@@ -3,7 +3,7 @@ const themeLoader = require('./loader');
3
3
  const storage = require('./storage');
4
4
  const getJSON = require('./to-json');
5
5
  const installer = require('./installer');
6
-
6
+ const validate = require('./validate');
7
7
  const settingsCache = require('../../../shared/settings-cache');
8
8
 
9
9
  module.exports = {
@@ -11,8 +11,9 @@ module.exports = {
11
11
  * Load the currently active theme
12
12
  */
13
13
  init: async () => {
14
- const themeName = settingsCache.get('active_theme');
14
+ validate.init();
15
15
 
16
+ const themeName = settingsCache.get('active_theme');
16
17
  return activate.loadAndActivate(themeName);
17
18
  },
18
19
  /**
@@ -25,6 +26,7 @@ module.exports = {
25
26
  api: {
26
27
  getJSON,
27
28
  activate: activate.activate,
29
+ getThemeErrors: validate.getThemeErrors,
28
30
  getZip: storage.getZip,
29
31
  setFromZip: storage.setFromZip,
30
32
  installFromGithub: installer.installFromGithub,
@@ -86,10 +86,12 @@ module.exports = {
86
86
  await activator.activateFromAPIOverride(themeName, loadedTheme, checkedTheme);
87
87
  }
88
88
 
89
+ const themeErrors = validate.getErrorsFromCheckedTheme(checkedTheme);
90
+
89
91
  // @TODO: unify the name across gscan and Ghost!
90
92
  return {
91
93
  themeOverridden: overrideTheme,
92
- theme: toJSON(themeName, checkedTheme)
94
+ theme: toJSON(themeName, themeErrors)
93
95
  };
94
96
  } catch (error) {
95
97
  // restore backup if we renamed an existing theme but saving failed
@@ -13,10 +13,10 @@ const settingsCache = require('../../../shared/settings-cache');
13
13
  * @TODO: settingsCache.get('active_theme') vs. active.get().name
14
14
  *
15
15
  * @param {string} [name] - the theme to output
16
- * @param {object} [checkedTheme] - a theme result from gscan
16
+ * @param {{errors: Array, warnings: Array}} [themeErrors] - Error and warning results from checked theme (if available)
17
17
  * @return {}
18
18
  */
19
- module.exports = function toJSON(name, checkedTheme) {
19
+ module.exports = function toJSON(name, themeErrors) {
20
20
  let themeResult;
21
21
  let toFilter;
22
22
 
@@ -30,12 +30,12 @@ module.exports = function toJSON(name, checkedTheme) {
30
30
 
31
31
  themeResult = packageJSON.filter(toFilter, settingsCache.get('active_theme'));
32
32
 
33
- if (checkedTheme && checkedTheme.results.warning.length > 0) {
34
- themeResult[0].warnings = _.cloneDeep(checkedTheme.results.warning);
33
+ if (themeErrors && themeErrors.warnings.length) {
34
+ themeResult[0].warnings = _.cloneDeep(themeErrors.warnings);
35
35
  }
36
36
 
37
- if (checkedTheme && checkedTheme.results.error.length > 0) {
38
- themeResult[0].errors = _.cloneDeep(checkedTheme.results.error);
37
+ if (themeErrors && themeErrors.errors.length) {
38
+ themeResult[0].errors = _.cloneDeep(themeErrors.errors);
39
39
  }
40
40
  }
41
41
 
@@ -5,20 +5,46 @@ const config = require('../../../shared/config');
5
5
  const labs = require('../../../shared/labs');
6
6
  const tpl = require('@tryghost/tpl');
7
7
  const errors = require('@tryghost/errors');
8
+ const adapterManager = require('../adapter-manager');
9
+ const logging = require('@tryghost/logging');
10
+ const list = require('./list');
8
11
 
9
12
  const messages = {
10
13
  themeHasErrors: 'Theme "{theme}" is not compatible or contains errors.',
11
14
  activeThemeHasFatalErrors: 'The currently active theme "{theme}" has fatal errors.',
12
- activeThemeHasErrors: 'The currently active theme "{theme}" has errors, but will still work.'
15
+ activeThemeHasErrors: 'The currently active theme "{theme}" has errors, but will still work.',
16
+ themeNotLoaded: 'Theme "{themeName}" is not loaded and cannot be checked.'
17
+ };
18
+
19
+ /**
20
+ * @typedef {Object} CacheStore
21
+ * @property {(key: string) => Promise<any>} get - get a value from the cache. Returns undefined if not found
22
+ * @property {(key: string, value: any) => Promise<void>} set - set a value in the cache
23
+ * @property {() => Promise<void>} reset - reset the cache
24
+ */
25
+
26
+ /**
27
+ * The cache store for storing the result of the last theme validation
28
+ * @type {CacheStore}
29
+ */
30
+ let gscanCacheStore;
31
+
32
+ module.exports.init = () => {
33
+ gscanCacheStore = adapterManager.getAdapter('cache:gscan');
13
34
  };
14
35
 
15
36
  const canActivate = function canActivate(checkedTheme) {
16
- // CASE: production and no fatal errors
17
- // CASE: development returns fatal and none fatal errors, theme is only invalid if fatal errors
18
- return !checkedTheme.results.error.length || (config.get('env') === 'development') && !checkedTheme.results.hasFatalErrors;
37
+ return !checkedTheme.results.hasFatalErrors;
19
38
  };
20
39
 
21
- const check = async function check(theme, isZip) {
40
+ const getErrorsFromCheckedTheme = function getErrorsFromCheckedTheme(checkedTheme) {
41
+ return {
42
+ errors: checkedTheme.results.error ?? [],
43
+ warnings: checkedTheme.results.warning ?? []
44
+ };
45
+ };
46
+
47
+ const check = async function check(themeName, theme, isZip) {
22
48
  debug('Begin: Check');
23
49
  // gscan can slow down boot time if we require on boot, for now nest the require.
24
50
  const gscan = require('gscan');
@@ -41,16 +67,59 @@ const check = async function check(theme, isZip) {
41
67
  }
42
68
 
43
69
  checkedTheme = gscan.format(checkedTheme, {
44
- onlyFatalErrors: config.get('env') === 'production',
70
+ onlyFatalErrors: false,
45
71
  checkVersion: checkedVersion
46
72
  });
47
73
 
74
+ // In production we don't want to show warnings
75
+ // Warnings are meant for developers only
76
+ if (config.get('env') === 'production') {
77
+ checkedTheme.results.warning = [];
78
+ }
79
+
80
+ // Cache warnings and errors
81
+ try {
82
+ await gscanCacheStore.set(themeName, getErrorsFromCheckedTheme(checkedTheme));
83
+ } catch (err) {
84
+ logging.error('Failed to cache gscan result');
85
+ logging.error(err);
86
+ }
87
+
48
88
  debug('End: Check');
49
89
  return checkedTheme;
50
90
  };
51
91
 
92
+ /**
93
+ * Returns the last cached errors and warnings of check() if available.
94
+ * Otherwise runs check() on the loaded theme with that name (which will always cache the error and warning results)
95
+ * @returns {Promise<{errors: Array, warnings: Array}>}
96
+ */
97
+ const getThemeErrors = async function getThemeErrors(themeName) {
98
+ try {
99
+ const cachedThemeErrors = await gscanCacheStore.get(themeName);
100
+ if (cachedThemeErrors) {
101
+ return cachedThemeErrors;
102
+ }
103
+ } catch (err) {
104
+ logging.error('Failed to get gscan result from cache');
105
+ logging.error(err);
106
+ }
107
+
108
+ const loadedTheme = list.get(themeName);
109
+
110
+ if (!loadedTheme) {
111
+ throw new errors.ValidationError({
112
+ message: tpl(messages.themeNotLoaded, {themeName: themeName}),
113
+ errorDetails: themeName
114
+ });
115
+ }
116
+
117
+ const result = await check(themeName, loadedTheme);
118
+ return getErrorsFromCheckedTheme(result);
119
+ };
120
+
52
121
  const checkSafe = async function checkSafe(themeName, theme, isZip) {
53
- const checkedTheme = await check(theme, isZip);
122
+ const checkedTheme = await check(themeName, theme, isZip);
54
123
 
55
124
  if (canActivate(checkedTheme)) {
56
125
  return checkedTheme;
@@ -74,7 +143,8 @@ const getThemeValidationError = (message, themeName, checkedTheme) => {
74
143
  message: tpl(messages[message], {theme: themeName}),
75
144
  errorDetails: Object.assign(
76
145
  _.pick(checkedTheme, ['checkedVersion', 'name', 'path', 'version']), {
77
- errors: checkedTheme.results.error
146
+ errors: checkedTheme.results.error,
147
+ warnings: checkedTheme.results.warning
78
148
  }
79
149
  )
80
150
  });
@@ -83,4 +153,6 @@ const getThemeValidationError = (message, themeName, checkedTheme) => {
83
153
  module.exports.check = check;
84
154
  module.exports.checkSafe = checkSafe;
85
155
  module.exports.canActivate = canActivate;
156
+ module.exports.getErrorsFromCheckedTheme = getErrorsFromCheckedTheme;
86
157
  module.exports.getThemeValidationError = getThemeValidationError;
158
+ module.exports.getThemeErrors = getThemeErrors;
@@ -159,6 +159,11 @@ module.exports = function apiRoutes() {
159
159
  http(api.themes.download)
160
160
  );
161
161
 
162
+ router.get('/themes/active',
163
+ mw.authAdminApi,
164
+ http(api.themes.readActive)
165
+ );
166
+
162
167
  router.post('/themes/upload',
163
168
  mw.authAdminApi,
164
169
  apiMw.upload.single('file'),
@@ -27,7 +27,8 @@
27
27
  "cache": {
28
28
  "active": "Memory",
29
29
  "settings": {},
30
- "imageSizes": {}
30
+ "imageSizes": {},
31
+ "gscan": {}
31
32
  }
32
33
  },
33
34
  "storage": {
@@ -169,7 +170,7 @@
169
170
  },
170
171
  "portal": {
171
172
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
172
- "version": "2.22"
173
+ "version": "2.23"
173
174
  },
174
175
  "sodoSearch": {
175
176
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -17,7 +17,8 @@ const messages = {
17
17
  const GA_FEATURES = [
18
18
  'sourceAttribution',
19
19
  'memberAttribution',
20
- 'audienceFeedback'
20
+ 'audienceFeedback',
21
+ 'themeErrorsNotification'
21
22
  ];
22
23
 
23
24
  // NOTE: this allowlist is meant to be used to filter out any unexpected