ghost 4.19.1 → 4.20.3

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 (139) hide show
  1. package/.eslintrc.js +9 -8
  2. package/Gruntfile.js +1 -1
  3. package/PRIVACY.md +3 -0
  4. package/content/adapters/README.md +2 -2
  5. package/core/boot.js +4 -4
  6. package/core/bridge.js +9 -1
  7. package/core/built/assets/{chunk.3.0778d8e4d707d2a625f1.js → chunk.3.777d43e2ce954ba8b2f5.js} +1 -1
  8. package/core/built/assets/codemirror/{codemirror-21a09582262987037db73b152fb35f7c.js → codemirror-d25c379b87ec8b33d54ac7149bc0b6ae.js} +14 -14
  9. package/core/built/assets/ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css +1 -0
  10. package/core/built/assets/{ghost.min-102753ec485602c8fe80d60a1750bf84.js → ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js} +525 -340
  11. package/core/built/assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css +1 -0
  12. package/core/built/assets/icons/arrow-left-small.svg +0 -4
  13. package/core/built/assets/img/footer-marketplace-bg-572b6c6486a7e26316954d599eaa9f30.png +0 -0
  14. package/core/built/assets/img/marketing/offers-1-f2e1b653c4d5bb90eea9d7a2862530f9.jpg +0 -0
  15. package/core/built/assets/img/marketing/offers-2-28a225d34cc39d133748431536961d00.jpg +0 -0
  16. package/core/built/assets/img/marketing/offers-3-2094c91ab21a16c37fbe6ec16c140160.jpg +0 -0
  17. package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
  18. package/core/built/assets/simplemde/{simplemde-232f69d126310434489071a1891e6d8b.js → simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js} +14 -14
  19. package/core/built/assets/{vendor.min-0916203b598271a795909e8e0b1c16c2.js → vendor.min-af502ac4142871500fc424f6a5a254ec.js} +1046 -1043
  20. package/core/frontend/apps/amp/lib/router.js +1 -1
  21. package/core/frontend/meta/author-url.js +1 -1
  22. package/core/frontend/meta/url.js +1 -1
  23. package/core/{server → frontend}/public/favicon.ico +0 -0
  24. package/core/{server → frontend}/public/ghost.css +0 -0
  25. package/core/{server → frontend}/public/ghost.min.css +0 -0
  26. package/core/{server → frontend}/public/robots.txt +0 -0
  27. package/core/{server → frontend}/public/sitemap.xsl +0 -0
  28. package/core/frontend/services/proxy.js +1 -1
  29. package/core/frontend/services/routing/CollectionRouter.js +3 -49
  30. package/core/frontend/services/routing/ParentRouter.js +1 -4
  31. package/core/frontend/services/routing/StaticPagesRouter.js +3 -5
  32. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -6
  33. package/core/frontend/services/routing/TaxonomyRouter.js +4 -5
  34. package/core/frontend/services/routing/controllers/collection.js +2 -2
  35. package/core/frontend/services/routing/controllers/email-post.js +2 -2
  36. package/core/frontend/services/routing/controllers/entry.js +2 -2
  37. package/core/frontend/services/routing/controllers/preview.js +2 -2
  38. package/core/frontend/services/routing/index.js +6 -12
  39. package/core/frontend/services/routing/registry.js +13 -0
  40. package/core/frontend/services/routing/router-manager.js +185 -0
  41. package/core/frontend/services/rss/generate-feed.js +2 -2
  42. package/core/frontend/services/theme-engine/i18n/i18n.js +267 -28
  43. package/core/frontend/services/theme-engine/i18n/index.js +1 -1
  44. package/core/frontend/services/theme-engine/i18n/theme-i18n.js +73 -0
  45. package/core/frontend/web/index.js +1 -0
  46. package/core/{server/web/site → frontend/web}/middleware/handle-image-sizes.js +4 -4
  47. package/core/{server/web/site → frontend/web}/middleware/index.js +0 -0
  48. package/core/{server/web/site → frontend/web}/middleware/redirect-ghost-to-admin.js +3 -3
  49. package/core/{server/web/site → frontend/web}/middleware/serve-favicon.js +6 -6
  50. package/core/{server/web/site → frontend/web}/middleware/serve-public-file.js +2 -2
  51. package/core/{server/web/site → frontend/web}/middleware/static-theme.js +3 -3
  52. package/core/{server/web/site → frontend/web}/routes.js +5 -4
  53. package/core/{server/web/site/app.js → frontend/web/site.js} +12 -16
  54. package/core/server/adapters/storage/LocalFileStorage.js +35 -39
  55. package/core/server/adapters/storage/index.js +12 -2
  56. package/core/server/api/canary/images.js +1 -1
  57. package/core/server/api/canary/offers.js +19 -0
  58. package/core/server/api/canary/utils/serializers/output/settings.js +2 -3
  59. package/core/server/api/canary/utils/serializers/output/utils/url.js +1 -1
  60. package/core/server/api/v2/images.js +1 -1
  61. package/core/server/api/v2/utils/serializers/output/utils/url.js +1 -1
  62. package/core/server/api/v3/images.js +1 -1
  63. package/core/server/api/v3/utils/serializers/output/settings.js +2 -3
  64. package/core/server/api/v3/utils/serializers/output/utils/url.js +1 -1
  65. package/core/server/data/importer/handlers/image.js +1 -1
  66. package/core/server/data/importer/importers/image.js +1 -1
  67. package/core/server/data/migrations/init/1-create-tables.js +7 -8
  68. package/core/server/data/migrations/init/2-create-fixtures.js +8 -8
  69. package/core/server/data/migrations/versions/4.20/01-remove-offer-redemptions-table.js +19 -0
  70. package/core/server/data/migrations/versions/4.20/02-remove-offers-table.js +30 -0
  71. package/core/server/data/migrations/versions/4.20/03-add-offers-table.js +21 -0
  72. package/core/server/data/migrations/versions/4.20/04-add-offer-redemptions-table.js +9 -0
  73. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +41 -0
  74. package/core/server/data/schema/fixtures/utils.js +150 -143
  75. package/core/server/data/schema/schema.js +4 -3
  76. package/core/server/frontend/ghost.min.css +1 -0
  77. package/core/server/lib/image/image-size.js +2 -2
  78. package/core/server/lib/mobiledoc.js +3 -2
  79. package/core/server/models/action.js +7 -4
  80. package/core/server/models/base/plugins/overrides.js +19 -6
  81. package/core/server/models/index.js +4 -46
  82. package/core/server/models/member.js +5 -0
  83. package/core/server/models/user.js +2 -1
  84. package/core/server/overrides.js +6 -2
  85. package/core/server/services/adapter-manager/config.js +1 -0
  86. package/core/server/services/adapter-manager/index.js +9 -5
  87. package/core/server/services/adapter-manager/options-resolver.js +18 -0
  88. package/core/server/services/bulk-email/mailgun.js +1 -1
  89. package/core/server/services/mega/post-email-serializer.js +2 -2
  90. package/core/server/services/members/api.js +1 -3
  91. package/core/server/services/members/emails/signin.js +1 -1
  92. package/core/server/services/members/emails/signup.js +1 -1
  93. package/core/server/services/members/emails/subscribe.js +1 -1
  94. package/core/server/services/members/service.js +2 -1
  95. package/core/server/services/offers/service.js +1 -1
  96. package/core/server/services/route-settings/route-settings.js +1 -1
  97. package/core/server/services/settings/index.js +3 -1
  98. package/core/server/services/settings/settings-bread-service.js +42 -20
  99. package/core/server/services/slack.js +1 -1
  100. package/core/server/services/themes/activate.js +2 -2
  101. package/core/server/services/themes/activation-bridge.js +6 -6
  102. package/core/server/services/themes/storage.js +1 -1
  103. package/core/{frontend → server}/services/url/Queue.js +0 -0
  104. package/core/{frontend → server}/services/url/Resource.js +0 -0
  105. package/core/{frontend → server}/services/url/Resources.js +2 -2
  106. package/core/{frontend → server}/services/url/UrlGenerator.js +14 -14
  107. package/core/{frontend → server}/services/url/UrlService.js +12 -15
  108. package/core/{frontend → server}/services/url/Urls.js +1 -1
  109. package/core/{frontend → server}/services/url/configs/canary.js +0 -0
  110. package/core/{frontend → server}/services/url/configs/v2.js +0 -0
  111. package/core/{frontend → server}/services/url/configs/v3.js +0 -0
  112. package/core/{frontend → server}/services/url/configs/v4.js +0 -0
  113. package/core/{frontend → server}/services/url/index.js +0 -0
  114. package/core/server/services/xmlrpc.js +1 -1
  115. package/core/server/update-check.js +3 -3
  116. package/core/server/web/admin/controller.js +11 -0
  117. package/core/server/web/admin/views/default-prod.html +4 -4
  118. package/core/server/web/admin/views/default.html +4 -4
  119. package/core/server/web/api/app.js +8 -9
  120. package/core/server/web/oauth/app.js +4 -2
  121. package/core/server/web/parent/backend.js +3 -3
  122. package/core/server/web/parent/frontend.js +2 -2
  123. package/core/server/web/shared/middlewares/custom-redirects.js +0 -8
  124. package/core/server/web/shared/middlewares/maintenance.js +1 -1
  125. package/core/server/web/well-known.js +10 -10
  126. package/core/shared/config/overrides.json +1 -1
  127. package/core/shared/express.js +10 -0
  128. package/core/shared/html-to-plaintext.js +2 -2
  129. package/core/shared/labs.js +14 -5
  130. package/package.json +45 -43
  131. package/yarn.lock +649 -284
  132. package/core/built/assets/ghost-dark-da8e8eba130fb52f97494e51850d1045.css +0 -1
  133. package/core/built/assets/ghost.min-0d8f19623e9f077351bce453034daf4d.css +0 -1
  134. package/core/frontend/services/routing/bootstrap.js +0 -134
  135. package/core/server/public/404-ghost.png +0 -0
  136. package/core/server/public/404-ghost@2x.png +0 -0
  137. package/core/server/web/site/index.js +0 -1
  138. package/core/shared/i18n/i18n.js +0 -312
  139. package/core/shared/i18n/index.js +0 -6
@@ -0,0 +1,18 @@
1
+ module.exports = function resolveAdapterOptions(name, adapterServiceConfig) {
2
+ const [adapterType, feature] = name.split(':');
3
+ const adapterSettings = adapterServiceConfig[adapterType];
4
+
5
+ let adapterName;
6
+ let adapterConfig;
7
+
8
+ // CASE: load resource-specific adapter when there is an adapter feature name specified as well as custom feature config
9
+ if (feature && adapterSettings[feature] && adapterSettings[adapterSettings[feature]]) {
10
+ adapterName = adapterSettings[feature];
11
+ adapterConfig = adapterSettings[adapterName];
12
+ } else {
13
+ adapterName = adapterSettings.active;
14
+ adapterConfig = adapterSettings[adapterName];
15
+ }
16
+
17
+ return {adapterType, adapterName, adapterConfig};
18
+ };
@@ -1,6 +1,5 @@
1
1
  const _ = require('lodash');
2
2
  const {URL} = require('url');
3
- const mailgun = require('mailgun-js');
4
3
  const logging = require('@tryghost/logging');
5
4
  const configService = require('../../../shared/config');
6
5
  const settingsCache = require('../../../shared/settings-cache');
@@ -8,6 +7,7 @@ const settingsCache = require('../../../shared/settings-cache');
8
7
  const BATCH_SIZE = 1000;
9
8
 
10
9
  function createMailgun(config) {
10
+ const mailgun = require('mailgun-js');
11
11
  const baseUrl = new URL(config.baseUrl);
12
12
 
13
13
  return mailgun({
@@ -1,5 +1,4 @@
1
1
  const _ = require('lodash');
2
- const juice = require('juice');
3
2
  const template = require('./template');
4
3
  const settingsCache = require('../../../shared/settings-cache');
5
4
  const urlUtils = require('../../../shared/url-utils');
@@ -20,6 +19,7 @@ const ALLOWED_REPLACEMENTS = ['first_name'];
20
19
  const formatHtmlForEmail = function formatHtmlForEmail(html) {
21
20
  const juiceOptions = {inlinePseudoElements: true};
22
21
 
22
+ const juice = require('juice');
23
23
  let juicedHtml = juice(html, juiceOptions);
24
24
 
25
25
  // convert juiced HTML to a DOM-like interface for further manipulation
@@ -227,7 +227,7 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
227
227
  const momentDate = post.published_at ? moment(post.published_at) : moment();
228
228
  post.published_at = momentDate.tz(timezone).format('DD MMM YYYY');
229
229
 
230
- post.authors = post.authors && post.authors.map(author => author.name).join(',');
230
+ post.authors = post.authors && post.authors.map(author => author.name).join(', ');
231
231
  if (post.posts_meta) {
232
232
  post.email_subject = post.posts_meta.email_subject;
233
233
  }
@@ -70,7 +70,6 @@ function createApiInstance(config) {
70
70
  For your security, the link will expire in 24 hours time.
71
71
 
72
72
  All the best!
73
- The team at ${siteTitle}
74
73
 
75
74
  ---
76
75
 
@@ -88,7 +87,6 @@ function createApiInstance(config) {
88
87
  For your security, the link will expire in 24 hours time.
89
88
 
90
89
  See you soon!
91
- The team at ${siteTitle}
92
90
 
93
91
  ---
94
92
 
@@ -122,7 +120,6 @@ function createApiInstance(config) {
122
120
  For your security, the link will expire in 24 hours time.
123
121
 
124
122
  See you soon!
125
- The team at ${siteTitle}
126
123
 
127
124
  ---
128
125
 
@@ -181,6 +178,7 @@ function createApiInstance(config) {
181
178
  MemberProductEvent: models.MemberProductEvent,
182
179
  MemberAnalyticEvent: models.MemberAnalyticEvent,
183
180
  OfferRedemption: models.OfferRedemption,
181
+ Offer: models.Offer,
184
182
  StripeProduct: models.StripeProduct,
185
183
  StripePrice: models.StripePrice,
186
184
  Product: models.Product,
@@ -134,7 +134,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
134
134
  </tbody>
135
135
  </table>
136
136
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!<br/>The team at ${siteTitle}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!</p>
138
138
  <hr/>
139
139
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;">${url}</p>
@@ -134,7 +134,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
134
134
  </tbody>
135
135
  </table>
136
136
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!<br/>The team at ${siteTitle}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!</p>
138
138
  <hr/>
139
139
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top: 0; color: #3A464C;">${url}</p>
@@ -134,7 +134,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
134
134
  </tbody>
135
135
  </table>
136
136
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">All the best!<br/>The team at ${siteTitle}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">All the best!</p>
138
138
  <hr/>
139
139
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top: 0; color: #3A464C;">${url}</p>
@@ -61,7 +61,8 @@ function reconfigureMembersAPI() {
61
61
  */
62
62
  const fetchImportThreshold = async () => {
63
63
  const membersTotal = await membersService.stats.getTotalMembers();
64
- const volumeThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold') || Infinity;
64
+ const configThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold');
65
+ const volumeThreshold = (configThreshold === undefined) ? Infinity : configThreshold;
65
66
  const threshold = Math.max(membersTotal, volumeThreshold);
66
67
 
67
68
  return threshold;
@@ -47,7 +47,7 @@ module.exports = {
47
47
  offersEnabled = labs.isSet('offers');
48
48
 
49
49
  if (offersEnabled) {
50
- const offers = await this.api.listOffers();
50
+ const offers = await this.api.listOffers({});
51
51
  for (const offer of offers) {
52
52
  redirectManager.addRedirect(`/${offer.code}`, `/#/portal/offers/${offer.id}`, {permanent: false});
53
53
  }
@@ -3,7 +3,7 @@ const moment = require('moment-timezone');
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
- const urlService = require('../../../frontend/services/url');
6
+ const urlService = require('../url');
7
7
 
8
8
  const debug = require('@tryghost/debug')('services:route-settings');
9
9
  const errors = require('@tryghost/errors');
@@ -4,6 +4,7 @@
4
4
  */
5
5
  const events = require('../../lib/common/events');
6
6
  const models = require('../../models');
7
+ const labs = require('../../../shared/labs');
7
8
  const SettingsCache = require('../../../shared/settings-cache');
8
9
  const SettingsBREADService = require('./settings-bread-service');
9
10
  const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
@@ -14,7 +15,8 @@ const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./setti
14
15
  const getSettingsBREADServiceInstance = () => {
15
16
  return new SettingsBREADService({
16
17
  SettingsModel: models.Settings,
17
- settingsCache: SettingsCache
18
+ settingsCache: SettingsCache,
19
+ labsService: labs
18
20
  });
19
21
  };
20
22
 
@@ -14,10 +14,12 @@ class SettingsBREADService {
14
14
  * @param {Object} options
15
15
  * @param {Object} options.SettingsModel
16
16
  * @param {Object} options.settingsCache - SettingsCache instance
17
+ * @param {Object} options.labsService - labs service instance
17
18
  */
18
- constructor({SettingsModel, settingsCache}) {
19
+ constructor({SettingsModel, settingsCache, labsService}) {
19
20
  this.SettingsModel = SettingsModel;
20
21
  this.settingsCache = settingsCache;
22
+ this.labs = labsService;
21
23
  }
22
24
 
23
25
  /**
@@ -28,24 +30,7 @@ class SettingsBREADService {
28
30
  browse(context) {
29
31
  let settings = this.settingsCache.getAll();
30
32
 
31
- // CASE: no context passed (functional call)
32
- if (!context) {
33
- return Promise.resolve(settings.filter((setting) => {
34
- return setting.group === 'site';
35
- }));
36
- }
37
-
38
- if (!context.internal) {
39
- // CASE: omit core settings unless internal request
40
- settings = _.filter(settings, (setting) => {
41
- const isCore = setting.group === 'core';
42
- return !isCore;
43
- });
44
- // CASE: omit secret settings unless internal request
45
- settings = settings.map(hideValueIfSecret);
46
- }
47
-
48
- return settings;
33
+ return this._formatBrowse(settings, context);
49
34
  }
50
35
 
51
36
  /**
@@ -86,6 +71,12 @@ class SettingsBREADService {
86
71
  }));
87
72
  }
88
73
 
74
+ // NOTE: Labs flags can exist outside of the DB when they are forced on/off
75
+ // so we grab them from the labs service instead as that's source-of-truth
76
+ if (setting.key === 'labs') {
77
+ setting.value = JSON.stringify(this.labs.getAll());
78
+ }
79
+
89
80
  setting = hideValueIfSecret(setting);
90
81
 
91
82
  return {
@@ -161,7 +152,9 @@ class SettingsBREADService {
161
152
  });
162
153
  }
163
154
 
164
- return this.SettingsModel.edit(filteredSettings, options);
155
+ return this.SettingsModel.edit(filteredSettings, options).then((result) => {
156
+ return this._formatBrowse(_.keyBy(_.invokeMap(result, 'toJSON'), 'key'), options.context);
157
+ });
165
158
  }
166
159
 
167
160
  /**
@@ -183,6 +176,35 @@ class SettingsBREADService {
183
176
  }
184
177
  }
185
178
  }
179
+
180
+ _formatBrowse(inputSettings, context) {
181
+ let settings = _.values(inputSettings);
182
+ // CASE: no context passed (functional call)
183
+ if (!context) {
184
+ return Promise.resolve(settings.filter((setting) => {
185
+ return setting.group === 'site';
186
+ }));
187
+ }
188
+
189
+ if (!context.internal) {
190
+ // CASE: omit core settings unless internal request
191
+ settings = _.filter(settings, (setting) => {
192
+ const isCore = setting.group === 'core';
193
+ return !isCore;
194
+ });
195
+ // CASE: omit secret settings unless internal request
196
+ settings = settings.map(hideValueIfSecret);
197
+ }
198
+
199
+ // NOTE: Labs flags can exist outside of the DB when they are forced on/off
200
+ // so we grab them from the labs service instead as that's source-of-truth
201
+ const labsSetting = settings.find(setting => setting.key === 'labs');
202
+ if (labsSetting) {
203
+ labsSetting.value = JSON.stringify(this.labs.getAll());
204
+ }
205
+
206
+ return settings;
207
+ }
186
208
  }
187
209
 
188
210
  module.exports = SettingsBREADService;
@@ -4,7 +4,7 @@ const logging = require('@tryghost/logging');
4
4
  const request = require('@tryghost/request');
5
5
  const {blogIcon} = require('../lib/image');
6
6
  const urlUtils = require('../../shared/url-utils');
7
- const urlService = require('../../frontend/services/url');
7
+ const urlService = require('./url');
8
8
  const settingsCache = require('../../shared/settings-cache');
9
9
  const schema = require('../data/schema').checks;
10
10
  const moment = require('moment');
@@ -30,7 +30,7 @@ module.exports.loadAndActivate = async (themeName) => {
30
30
  logging.warn(validate.getThemeValidationError('activeThemeHasErrors', themeName, checkedTheme));
31
31
  }
32
32
 
33
- activator.activateFromBoot(themeName, loadedTheme, checkedTheme);
33
+ await activator.activateFromBoot(themeName, loadedTheme, checkedTheme);
34
34
  } catch (err) {
35
35
  if (err instanceof errors.NotFoundError) {
36
36
  // CASE: active theme is missing, we don't want to exit because the admin panel will still work
@@ -56,7 +56,7 @@ module.exports.activate = async (themeName) => {
56
56
  // Validate
57
57
  const checkedTheme = await validate.checkSafe(themeName, loadedTheme);
58
58
  // Activate
59
- activator.activateFromAPI(themeName, loadedTheme, checkedTheme);
59
+ await activator.activateFromAPI(themeName, loadedTheme, checkedTheme);
60
60
  // Return the checked theme
61
61
  return checkedTheme;
62
62
  };
@@ -8,27 +8,27 @@ const customThemeSettings = require('../custom-theme-settings');
8
8
  * And also adds a little debug statement, which is very handy when debugging theme logic
9
9
  */
10
10
  module.exports = {
11
- activateFromBoot: (themeName, theme, checkedTheme) => {
11
+ activateFromBoot: async (themeName, theme, checkedTheme) => {
12
12
  debug('Activating theme (method A on boot)', themeName);
13
13
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
14
14
  if (labs.isSet('customThemeSettings')) {
15
- customThemeSettings.api.activateTheme(checkedTheme);
15
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
16
16
  }
17
17
  bridge.activateTheme(theme, checkedTheme);
18
18
  },
19
- activateFromAPI: (themeName, theme, checkedTheme) => {
19
+ activateFromAPI: async (themeName, theme, checkedTheme) => {
20
20
  debug('Activating theme (method B on API "activate")', themeName);
21
21
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
22
22
  if (labs.isSet('customThemeSettings')) {
23
- customThemeSettings.api.activateTheme(checkedTheme);
23
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
24
24
  }
25
25
  bridge.activateTheme(theme, checkedTheme);
26
26
  },
27
- activateFromAPIOverride: (themeName, theme, checkedTheme) => {
27
+ activateFromAPIOverride: async (themeName, theme, checkedTheme) => {
28
28
  debug('Activating theme (method C on API "override")', themeName);
29
29
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
30
30
  if (labs.isSet('customThemeSettings')) {
31
- customThemeSettings.api.activateTheme(checkedTheme);
31
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
32
32
  }
33
33
  bridge.activateTheme(theme, checkedTheme);
34
34
  }
@@ -83,7 +83,7 @@ module.exports = {
83
83
  // CASE: if this is the active theme, we are overriding
84
84
  if (overrideTheme) {
85
85
  debug('setFromZip Theme is active already');
86
- activator.activateFromAPIOverride(themeName, loadedTheme, checkedTheme);
86
+ await activator.activateFromAPIOverride(themeName, loadedTheme, checkedTheme);
87
87
  }
88
88
 
89
89
  // @TODO: unify the name across gscan and Ghost!
File without changes
@@ -3,10 +3,10 @@ const Promise = require('bluebird');
3
3
  const debug = require('@tryghost/debug')('services:url:resources');
4
4
  const Resource = require('./Resource');
5
5
  const config = require('../../../shared/config');
6
- const models = require('../../../server/models');
6
+ const models = require('../../models');
7
7
 
8
8
  // This listens to all manner of model events to find new content that needs a URL...
9
- const events = require('../../../server/lib/common/events');
9
+ const events = require('../../lib/common/events');
10
10
 
11
11
  /**
12
12
  * @description At the moment the resources class is directly responsible for data population
@@ -68,24 +68,24 @@ class UrlGenerator {
68
68
  }
69
69
 
70
70
  /**
71
- * @description Helper function to register listeners for each url generator instance.
72
- * @private
71
+ * @NOTE: currently only used if the permalink setting changes and it's used for this url generator.
72
+ * @TODO: https://github.com/TryGhost/Ghost/issues/10699
73
73
  */
74
- _listeners() {
75
- /**
76
- * @NOTE: currently only used if the permalink setting changes and it's used for this url generator.
77
- * @TODO: https://github.com/TryGhost/Ghost/issues/10699
78
- */
79
- this.router.addListener('updated', () => {
80
- const myResources = this.urls.getByGeneratorId(this.uid);
74
+ regenerateResources() {
75
+ const myResources = this.urls.getByGeneratorId(this.uid);
81
76
 
82
- myResources.forEach((object) => {
83
- this.urls.removeResourceId(object.resource.data.id);
84
- object.resource.release();
85
- this._try(object.resource);
86
- });
77
+ myResources.forEach((object) => {
78
+ this.urls.removeResourceId(object.resource.data.id);
79
+ object.resource.release();
80
+ this._try(object.resource);
87
81
  });
82
+ }
88
83
 
84
+ /**
85
+ * @description Helper function to register listeners for each url generator instance.
86
+ * @private
87
+ */
88
+ _listeners() {
89
89
  /**
90
90
  * Listen on two events:
91
91
  *
@@ -9,7 +9,7 @@ const Resources = require('./Resources');
9
9
  const urlUtils = require('../../../shared/url-utils');
10
10
 
11
11
  // This listens to services.themes.api.changed, routing events, and it's own queue events
12
- const events = require('../../../server/lib/common/events');
12
+ const events = require('../../lib/common/events');
13
13
 
14
14
  /**
15
15
  * The url service class holds all instances in a centralized place.
@@ -35,9 +35,6 @@ class UrlService {
35
35
  * @private
36
36
  */
37
37
  _listeners() {
38
- this._onRouterAddedListener = this._onRouterAddedType.bind(this);
39
- events.on('router.created', this._onRouterAddedListener);
40
-
41
38
  this._onThemeChangedListener = this._onThemeChangedListener.bind(this);
42
39
  events.on('services.themes.api.changed', this._onThemeChangedListener);
43
40
 
@@ -77,22 +74,23 @@ class UrlService {
77
74
  /**
78
75
  * @description Router was created, connect it with a url generator.
79
76
  * @param {ExpressRouter} router
80
- * @private
81
77
  */
82
- _onRouterAddedType(router) {
83
- // CASE: there are router types which do not generate resource urls
84
- // e.g. static route router
85
- // we are listening on the general `router.created` event - every router throws this event
86
- if (!router || !router.getPermalinks()) {
87
- return;
88
- }
89
-
90
- debug('router.created');
78
+ onRouterAddedType(router) {
79
+ debug('Registering route: ', router.name);
91
80
 
92
81
  let urlGenerator = new UrlGenerator(router, this.queue, this.resources, this.urls, this.urlGenerators.length);
93
82
  this.urlGenerators.push(urlGenerator);
94
83
  }
95
84
 
85
+ /**
86
+ * @description Router update handler - regenerates it's resources
87
+ * @param {ExpressRouter} router
88
+ */
89
+ onRouterUpdated(router) {
90
+ const generator = this.urlGenerators.find(g => g.router.id === router.id);
91
+ generator.regenerateResources();
92
+ }
93
+
96
94
  /**
97
95
  * @description If the API version in the theme config changes, we have to reset urls and resources.
98
96
  * @private
@@ -307,7 +305,6 @@ class UrlService {
307
305
  if (!options.keepListeners) {
308
306
  this._onQueueStartedListener && this.queue.removeListener('started', this._onQueueStartedListener);
309
307
  this._onQueueEndedListener && this.queue.removeListener('ended', this._onQueueEndedListener);
310
- this._onRouterAddedListener && events.removeListener('router.created', this._onRouterAddedListener);
311
308
  this._onThemeChangedListener && events.removeListener('services.themes.api.changed', this._onThemeChangedListener);
312
309
  }
313
310
  }
@@ -5,7 +5,7 @@ const logging = require('@tryghost/logging');
5
5
  const errors = require('@tryghost/errors');
6
6
 
7
7
  // This emits its own url added/removed events
8
- const events = require('../../../server/lib/common/events');
8
+ const events = require('../../lib/common/events');
9
9
 
10
10
  /**
11
11
  * This class keeps track of all urls in the system.
File without changes
@@ -1,7 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const xml = require('xml');
3
3
  const config = require('../../shared/config');
4
- const urlService = require('../../frontend/services/url');
4
+ const urlService = require('./url');
5
5
  const errors = require('@tryghost/errors');
6
6
  const tpl = require('@tryghost/tpl');
7
7
  const logging = require('@tryghost/logging');
@@ -1,7 +1,6 @@
1
1
  const _ = require('lodash');
2
2
 
3
3
  const api = require('./api');
4
- const GhostMailer = require('./services/mail').GhostMailer;
5
4
  const config = require('../shared/config');
6
5
  const urlUtils = require('./../shared/url-utils');
7
6
  const jobsService = require('./services/jobs');
@@ -10,8 +9,6 @@ const request = require('@tryghost/request');
10
9
  const ghostVersion = require('@tryghost/version');
11
10
  const UpdateCheckService = require('@tryghost/update-check-service');
12
11
 
13
- const ghostMailer = new GhostMailer();
14
-
15
12
  /**
16
13
  * Initializes and triggers update check
17
14
  *
@@ -25,6 +22,9 @@ module.exports = async () => {
25
22
  return;
26
23
  }
27
24
 
25
+ const {GhostMailer} = require('./services/mail');
26
+ const ghostMailer = new GhostMailer();
27
+
28
28
  const updateChecker = new UpdateCheckService({
29
29
  api: {
30
30
  settings: {
@@ -1,5 +1,7 @@
1
1
  const debug = require('@tryghost/debug')('web:admin:controller');
2
2
  const path = require('path');
3
+ const fs = require('fs');
4
+ const crypto = require('crypto');
3
5
  const config = require('../../../shared/config');
4
6
  const updateCheck = require('../../update-check');
5
7
 
@@ -21,6 +23,15 @@ module.exports = function adminController(req, res) {
21
23
  const templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate);
22
24
  const headers = {};
23
25
 
26
+ // Generate our own ETag header
27
+ // `sendFile` by default uses filesize+lastmod date to generate an etag.
28
+ // That doesn't work for admin templates because the filesize doesn't change between versions
29
+ // and `npm pack` sets a fixed lastmod date for every file meaning the default etag never changes
30
+ const fileBuffer = fs.readFileSync(templatePath);
31
+ const hashSum = crypto.createHash('md5');
32
+ hashSum.update(fileBuffer);
33
+ headers.ETag = hashSum.digest('hex');
34
+
24
35
  if (config.get('adminFrameProtection')) {
25
36
  headers['X-Frame-Options'] = 'sameorigin';
26
37
  }
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.19%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.20%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -41,7 +41,7 @@
41
41
 
42
42
 
43
43
  <link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
44
- <link rel="stylesheet" href="assets/ghost.min-0d8f19623e9f077351bce453034daf4d.css" title="light">
44
+ <link rel="stylesheet" href="assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css" title="light">
45
45
 
46
46
 
47
47
 
@@ -59,8 +59,8 @@
59
59
  <div id="ember-basic-dropdown-wormhole"></div>
60
60
 
61
61
 
62
- <script src="assets/vendor.min-0916203b598271a795909e8e0b1c16c2.js"></script>
63
- <script src="assets/ghost.min-102753ec485602c8fe80d60a1750bf84.js"></script>
62
+ <script src="assets/vendor.min-af502ac4142871500fc424f6a5a254ec.js"></script>
63
+ <script src="assets/ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js"></script>
64
64
 
65
65
  </body>
66
66
  </html>
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.19%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.20%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -41,7 +41,7 @@
41
41
 
42
42
 
43
43
  <link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
44
- <link rel="stylesheet" href="assets/ghost.min-0d8f19623e9f077351bce453034daf4d.css" title="light">
44
+ <link rel="stylesheet" href="assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css" title="light">
45
45
 
46
46
 
47
47
 
@@ -59,8 +59,8 @@
59
59
  <div id="ember-basic-dropdown-wormhole"></div>
60
60
 
61
61
 
62
- <script src="assets/vendor.min-0916203b598271a795909e8e0b1c16c2.js"></script>
63
- <script src="assets/ghost.min-102753ec485602c8fe80d60a1750bf84.js"></script>
62
+ <script src="assets/vendor.min-af502ac4142871500fc424f6a5a254ec.js"></script>
63
+ <script src="assets/ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js"></script>
64
64
 
65
65
  </body>
66
66
  </html>