ghost 5.61.3 → 5.62.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 (156) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.62.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.62.0.tgz +0 -0
  3. package/components/{tryghost-adapter-manager-5.61.3.tgz → tryghost-adapter-manager-5.62.0.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.62.0.tgz +0 -0
  5. package/components/tryghost-api-framework-5.62.0.tgz +0 -0
  6. package/components/{tryghost-api-version-compatibility-service-5.61.3.tgz → tryghost-api-version-compatibility-service-5.62.0.tgz} +0 -0
  7. package/components/{tryghost-audience-feedback-5.61.3.tgz → tryghost-audience-feedback-5.62.0.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.62.0.tgz +0 -0
  9. package/components/tryghost-bootstrap-socket-5.62.0.tgz +0 -0
  10. package/components/tryghost-collections-5.62.0.tgz +0 -0
  11. package/components/{tryghost-constants-5.61.3.tgz → tryghost-constants-5.62.0.tgz} +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.62.0.tgz +0 -0
  13. package/components/tryghost-data-generator-5.62.0.tgz +0 -0
  14. package/components/tryghost-domain-events-5.62.0.tgz +0 -0
  15. package/components/tryghost-donations-5.62.0.tgz +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.62.0.tgz +0 -0
  17. package/components/tryghost-email-analytics-provider-mailgun-5.62.0.tgz +0 -0
  18. package/components/{tryghost-email-analytics-service-5.61.3.tgz → tryghost-email-analytics-service-5.62.0.tgz} +0 -0
  19. package/components/tryghost-email-content-generator-5.62.0.tgz +0 -0
  20. package/components/{tryghost-email-events-5.61.3.tgz → tryghost-email-events-5.62.0.tgz} +0 -0
  21. package/components/{tryghost-email-service-5.61.3.tgz → tryghost-email-service-5.62.0.tgz} +0 -0
  22. package/components/tryghost-email-suppression-list-5.62.0.tgz +0 -0
  23. package/components/tryghost-event-aware-cache-wrapper-5.62.0.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.61.3.tgz → tryghost-express-dynamic-redirects-5.62.0.tgz} +0 -0
  25. package/components/{tryghost-external-media-inliner-5.61.3.tgz → tryghost-external-media-inliner-5.62.0.tgz} +0 -0
  26. package/components/{tryghost-extract-api-key-5.61.3.tgz → tryghost-extract-api-key-5.62.0.tgz} +0 -0
  27. package/components/{tryghost-html-to-plaintext-5.61.3.tgz → tryghost-html-to-plaintext-5.62.0.tgz} +0 -0
  28. package/components/tryghost-i18n-5.62.0.tgz +0 -0
  29. package/components/tryghost-importer-handler-content-files-5.62.0.tgz +0 -0
  30. package/components/{tryghost-importer-revue-5.61.3.tgz → tryghost-importer-revue-5.62.0.tgz} +0 -0
  31. package/components/tryghost-in-memory-repository-5.62.0.tgz +0 -0
  32. package/components/{tryghost-job-manager-5.61.3.tgz → tryghost-job-manager-5.62.0.tgz} +0 -0
  33. package/components/tryghost-link-redirects-5.62.0.tgz +0 -0
  34. package/components/tryghost-link-replacer-5.62.0.tgz +0 -0
  35. package/components/{tryghost-link-tracking-5.61.3.tgz → tryghost-link-tracking-5.62.0.tgz} +0 -0
  36. package/components/{tryghost-magic-link-5.61.3.tgz → tryghost-magic-link-5.62.0.tgz} +0 -0
  37. package/components/tryghost-mail-events-5.62.0.tgz +0 -0
  38. package/components/tryghost-mailgun-client-5.62.0.tgz +0 -0
  39. package/components/{tryghost-member-attribution-5.61.3.tgz → tryghost-member-attribution-5.62.0.tgz} +0 -0
  40. package/components/tryghost-member-events-5.62.0.tgz +0 -0
  41. package/components/tryghost-members-api-5.62.0.tgz +0 -0
  42. package/components/{tryghost-members-csv-5.61.3.tgz → tryghost-members-csv-5.62.0.tgz} +0 -0
  43. package/components/{tryghost-members-events-service-5.61.3.tgz → tryghost-members-events-service-5.62.0.tgz} +0 -0
  44. package/components/{tryghost-members-importer-5.61.3.tgz → tryghost-members-importer-5.62.0.tgz} +0 -0
  45. package/components/tryghost-members-offers-5.62.0.tgz +0 -0
  46. package/components/tryghost-members-payments-5.62.0.tgz +0 -0
  47. package/components/tryghost-members-ssr-5.62.0.tgz +0 -0
  48. package/components/{tryghost-members-stripe-service-5.61.3.tgz → tryghost-members-stripe-service-5.62.0.tgz} +0 -0
  49. package/components/tryghost-mentions-email-report-5.62.0.tgz +0 -0
  50. package/components/tryghost-milestones-5.62.0.tgz +0 -0
  51. package/components/{tryghost-minifier-5.61.3.tgz → tryghost-minifier-5.62.0.tgz} +0 -0
  52. package/components/tryghost-model-to-domain-event-interceptor-5.62.0.tgz +0 -0
  53. package/components/tryghost-mw-api-version-mismatch-5.62.0.tgz +0 -0
  54. package/components/tryghost-mw-cache-control-5.62.0.tgz +0 -0
  55. package/components/{tryghost-mw-error-handler-5.61.3.tgz → tryghost-mw-error-handler-5.62.0.tgz} +0 -0
  56. package/components/tryghost-mw-session-from-token-5.62.0.tgz +0 -0
  57. package/components/tryghost-mw-update-user-last-seen-5.62.0.tgz +0 -0
  58. package/components/tryghost-mw-version-match-5.62.0.tgz +0 -0
  59. package/components/{tryghost-mw-vhost-5.61.3.tgz → tryghost-mw-vhost-5.62.0.tgz} +0 -0
  60. package/components/tryghost-nql-filter-expansions-5.62.0.tgz +0 -0
  61. package/components/{tryghost-oembed-service-5.61.3.tgz → tryghost-oembed-service-5.62.0.tgz} +0 -0
  62. package/components/{tryghost-package-json-5.61.3.tgz → tryghost-package-json-5.62.0.tgz} +0 -0
  63. package/components/tryghost-post-events-5.62.0.tgz +0 -0
  64. package/components/{tryghost-post-revisions-5.61.3.tgz → tryghost-post-revisions-5.62.0.tgz} +0 -0
  65. package/components/tryghost-posts-service-5.62.0.tgz +0 -0
  66. package/components/tryghost-recommendations-5.62.0.tgz +0 -0
  67. package/components/tryghost-referrers-5.62.0.tgz +0 -0
  68. package/components/{tryghost-security-5.61.3.tgz → tryghost-security-5.62.0.tgz} +0 -0
  69. package/components/{tryghost-session-service-5.61.3.tgz → tryghost-session-service-5.62.0.tgz} +0 -0
  70. package/components/tryghost-settings-path-manager-5.62.0.tgz +0 -0
  71. package/components/tryghost-slack-notifications-5.62.0.tgz +0 -0
  72. package/components/{tryghost-staff-service-5.61.3.tgz → tryghost-staff-service-5.62.0.tgz} +0 -0
  73. package/components/{tryghost-stats-service-5.61.3.tgz → tryghost-stats-service-5.62.0.tgz} +0 -0
  74. package/components/{tryghost-tiers-5.61.3.tgz → tryghost-tiers-5.62.0.tgz} +0 -0
  75. package/components/{tryghost-update-check-service-5.61.3.tgz → tryghost-update-check-service-5.62.0.tgz} +0 -0
  76. package/components/tryghost-verification-trigger-5.62.0.tgz +0 -0
  77. package/components/tryghost-version-notifications-data-service-5.62.0.tgz +0 -0
  78. package/components/tryghost-webmentions-5.62.0.tgz +0 -0
  79. package/core/built/admin/assets/{chunk.143.da84b3b434141aff0f01.js → chunk.143.5b5502a550ce35005d0f.js} +6 -5
  80. package/core/built/admin/assets/{chunk.178.659d369f286d4162b294.js → chunk.178.546664edca0b1b0f2ab2.js} +4 -4
  81. package/core/built/admin/assets/{chunk.208.dbf172ad32f72f21a5dc.js → chunk.853.f743ed975e8838475532.js} +91 -27
  82. package/core/built/admin/assets/{ghost-54f84395df6a6fb47b37008ded8eba22.js → ghost-d804aba7bca07fa75d308ab892c508fc.js} +13 -13
  83. package/core/built/admin/assets/{vendor-3631184082d609038638c1e169a002e7.js → vendor-b50a3e5c2079b8a35d9122a1a4c34ef6.js} +248 -321
  84. package/core/built/admin/index.html +5 -5
  85. package/core/server/adapters/storage/LocalStorageBase.js +1 -8
  86. package/core/server/api/endpoints/recommendations-public.js +2 -5
  87. package/core/server/api/endpoints/recommendations.js +6 -3
  88. package/core/server/api/endpoints/utils/serializers/input/pages.js +8 -0
  89. package/core/server/api/endpoints/utils/serializers/input/posts.js +9 -1
  90. package/core/server/api/endpoints/utils/serializers/output/site.js +2 -0
  91. package/core/server/lib/lexical.js +15 -0
  92. package/core/server/models/base/plugins/crud.js +6 -0
  93. package/core/server/models/base/plugins/sanitize.js +1 -1
  94. package/core/server/models/post.js +11 -1
  95. package/core/server/services/mentions/WebmentionMetadata.js +38 -1
  96. package/core/server/services/mentions/service.js +2 -1
  97. package/core/server/services/offers/OfferBookshelfRepository.js +230 -0
  98. package/core/server/services/offers/service.js +7 -3
  99. package/core/server/services/public-config/site.js +3 -1
  100. package/core/server/services/recommendations/RecommendationEnablerService.js +28 -0
  101. package/core/server/services/recommendations/RecommendationServiceWrapper.js +23 -1
  102. package/core/server/services/settings/SettingsBREADService.js +1 -1
  103. package/core/server/services/settings/settings-service.js +1 -0
  104. package/core/server/services/settings-helpers/SettingsHelpers.js +7 -0
  105. package/core/server/web/members/app.js +7 -0
  106. package/core/shared/config/defaults.json +2 -2
  107. package/core/shared/settings-cache/public.js +1 -0
  108. package/package.json +150 -149
  109. package/yarn.lock +197 -229
  110. package/components/tryghost-adapter-cache-memory-ttl-5.61.3.tgz +0 -0
  111. package/components/tryghost-adapter-cache-redis-5.61.3.tgz +0 -0
  112. package/components/tryghost-announcement-bar-settings-5.61.3.tgz +0 -0
  113. package/components/tryghost-api-framework-5.61.3.tgz +0 -0
  114. package/components/tryghost-bookshelf-repository-5.61.3.tgz +0 -0
  115. package/components/tryghost-bootstrap-socket-5.61.3.tgz +0 -0
  116. package/components/tryghost-collections-5.61.3.tgz +0 -0
  117. package/components/tryghost-custom-theme-settings-service-5.61.3.tgz +0 -0
  118. package/components/tryghost-data-generator-5.61.3.tgz +0 -0
  119. package/components/tryghost-domain-events-5.61.3.tgz +0 -0
  120. package/components/tryghost-donations-5.61.3.tgz +0 -0
  121. package/components/tryghost-dynamic-routing-events-5.61.3.tgz +0 -0
  122. package/components/tryghost-email-analytics-provider-mailgun-5.61.3.tgz +0 -0
  123. package/components/tryghost-email-content-generator-5.61.3.tgz +0 -0
  124. package/components/tryghost-email-suppression-list-5.61.3.tgz +0 -0
  125. package/components/tryghost-event-aware-cache-wrapper-5.61.3.tgz +0 -0
  126. package/components/tryghost-i18n-5.61.3.tgz +0 -0
  127. package/components/tryghost-importer-handler-content-files-5.61.3.tgz +0 -0
  128. package/components/tryghost-in-memory-repository-5.61.3.tgz +0 -0
  129. package/components/tryghost-link-redirects-5.61.3.tgz +0 -0
  130. package/components/tryghost-link-replacer-5.61.3.tgz +0 -0
  131. package/components/tryghost-mail-events-5.61.3.tgz +0 -0
  132. package/components/tryghost-mailgun-client-5.61.3.tgz +0 -0
  133. package/components/tryghost-member-events-5.61.3.tgz +0 -0
  134. package/components/tryghost-members-api-5.61.3.tgz +0 -0
  135. package/components/tryghost-members-offers-5.61.3.tgz +0 -0
  136. package/components/tryghost-members-payments-5.61.3.tgz +0 -0
  137. package/components/tryghost-members-ssr-5.61.3.tgz +0 -0
  138. package/components/tryghost-mentions-email-report-5.61.3.tgz +0 -0
  139. package/components/tryghost-milestones-5.61.3.tgz +0 -0
  140. package/components/tryghost-model-to-domain-event-interceptor-5.61.3.tgz +0 -0
  141. package/components/tryghost-mw-api-version-mismatch-5.61.3.tgz +0 -0
  142. package/components/tryghost-mw-cache-control-5.61.3.tgz +0 -0
  143. package/components/tryghost-mw-session-from-token-5.61.3.tgz +0 -0
  144. package/components/tryghost-mw-update-user-last-seen-5.61.3.tgz +0 -0
  145. package/components/tryghost-mw-version-match-5.61.3.tgz +0 -0
  146. package/components/tryghost-nql-filter-expansions-5.61.3.tgz +0 -0
  147. package/components/tryghost-post-events-5.61.3.tgz +0 -0
  148. package/components/tryghost-posts-service-5.61.3.tgz +0 -0
  149. package/components/tryghost-recommendations-5.61.3.tgz +0 -0
  150. package/components/tryghost-referrers-5.61.3.tgz +0 -0
  151. package/components/tryghost-settings-path-manager-5.61.3.tgz +0 -0
  152. package/components/tryghost-slack-notifications-5.61.3.tgz +0 -0
  153. package/components/tryghost-verification-trigger-5.61.3.tgz +0 -0
  154. package/components/tryghost-version-notifications-data-service-5.61.3.tgz +0 -0
  155. package/components/tryghost-webmentions-5.61.3.tgz +0 -0
  156. /package/core/built/admin/assets/{chunk.208.dbf172ad32f72f21a5dc.js.LICENSE.txt → chunk.853.f743ed975e8838475532.js.LICENSE.txt} +0 -0
@@ -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%22cdnUrl%22%3A%22%22%2C%22rootURL%22%3A%22%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%225.61%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%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%22cdnUrl%22%3A%22%22%2C%22rootURL%22%3A%22%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%225.62%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%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" />
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-3631184082d609038638c1e169a002e7.js"></script>
60
- <script src="assets/chunk.208.dbf172ad32f72f21a5dc.js"></script>
61
- <script src="assets/chunk.143.da84b3b434141aff0f01.js"></script>
62
- <script src="assets/ghost-54f84395df6a6fb47b37008ded8eba22.js"></script>
59
+ <script src="assets/vendor-b50a3e5c2079b8a35d9122a1a4c34ef6.js"></script>
60
+ <script src="assets/chunk.853.f743ed975e8838475532.js"></script>
61
+ <script src="assets/chunk.143.5b5502a550ce35005d0f.js"></script>
62
+ <script src="assets/ghost-d804aba7bca07fa75d308ab892c508fc.js"></script>
63
63
  </body>
64
64
  </html>
@@ -4,9 +4,7 @@ const serveStatic = require('../../../shared/express').static;
4
4
 
5
5
  const fs = require('fs-extra');
6
6
  const path = require('path');
7
- const moment = require('moment');
8
7
  const tpl = require('@tryghost/tpl');
9
- const logging = require('@tryghost/logging');
10
8
  const errors = require('@tryghost/errors');
11
9
  const constants = require('@tryghost/constants');
12
10
  const urlUtils = require('../../../shared/url-utils');
@@ -127,16 +125,11 @@ class LocalStorageBase extends StorageBase {
127
125
  const {storagePath, errorMessages} = this;
128
126
 
129
127
  return function serveStaticContent(req, res, next) {
130
- const startedAtMoment = moment();
131
-
132
128
  return serveStatic(
133
129
  storagePath,
134
130
  {
135
131
  maxAge: constants.ONE_YEAR_MS,
136
- fallthrough: false,
137
- onEnd: () => {
138
- logging.info('LocalStorageBase.serve', req.path, moment().diff(startedAtMoment, 'ms') + 'ms');
139
- }
132
+ fallthrough: false
140
133
  }
141
134
  )(req, res, (err) => {
142
135
  if (err) {
@@ -9,16 +9,13 @@ module.exports = {
9
9
  },
10
10
  options: [
11
11
  'limit',
12
- 'fields',
13
- 'filter',
14
12
  'order',
15
- 'debug',
16
13
  'page'
17
14
  ],
18
15
  permissions: true,
19
16
  validation: {},
20
- async query() {
21
- return await recommendations.controller.listRecommendations();
17
+ async query(frame) {
18
+ return await recommendations.controller.listRecommendations(frame);
22
19
  }
23
20
  }
24
21
  };
@@ -7,11 +7,14 @@ module.exports = {
7
7
  headers: {
8
8
  cacheInvalidate: false
9
9
  },
10
- options: [],
10
+ options: [
11
+ 'limit',
12
+ 'page'
13
+ ],
11
14
  permissions: true,
12
15
  validation: {},
13
- async query() {
14
- return await recommendations.controller.listRecommendations();
16
+ async query(frame) {
17
+ return await recommendations.controller.listRecommendations(frame);
15
18
  }
16
19
  },
17
20
 
@@ -6,6 +6,8 @@ const slugFilterOrder = require('./utils/slug-filter-order');
6
6
  const localUtils = require('../../index');
7
7
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
8
8
  const clean = require('./utils/clean');
9
+ const labs = require('../../../../../../shared/labs');
10
+ const lexical = require('../../../../../lib/lexical');
9
11
 
10
12
  function removeSourceFormats(frame) {
11
13
  if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
@@ -133,6 +135,12 @@ module.exports = {
133
135
 
134
136
  if (frame.options.source === 'html' && !_.isEmpty(html)) {
135
137
  frame.data.pages[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html));
138
+
139
+ // normally we don't allow both mobiledoc+lexical but the model layer will remove lexical
140
+ // if mobiledoc is already present to avoid migrating formats outside of an explicit conversion
141
+ if (labs.isSet('lexicalEditor')) {
142
+ frame.data.pages[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html));
143
+ }
136
144
  }
137
145
  }
138
146
 
@@ -6,6 +6,8 @@ const localUtils = require('../../index');
6
6
  const mobiledoc = require('../../../../../lib/mobiledoc');
7
7
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
8
8
  const clean = require('./utils/clean');
9
+ const labs = require('../../../../../../shared/labs');
10
+ const lexical = require('../../../../../lib/lexical');
9
11
 
10
12
  function removeSourceFormats(frame) {
11
13
  if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
@@ -32,7 +34,7 @@ function defaultRelations(frame) {
32
34
  // Apply same mapping as content API
33
35
  mapWithRelated(frame);
34
36
 
35
- // Addditional defaults for admin API
37
+ // Additional defaults for admin API
36
38
  if (frame.options.withRelated) {
37
39
  return;
38
40
  }
@@ -167,6 +169,12 @@ module.exports = {
167
169
 
168
170
  if (frame.options.source === 'html' && !_.isEmpty(html)) {
169
171
  frame.data.posts[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html));
172
+
173
+ // normally we don't allow both mobiledoc+lexical but the model layer will remove lexical
174
+ // if mobiledoc is already present to avoid migrating formats outside of an explicit conversion
175
+ if (labs.isSet('lexicalEditor')) {
176
+ frame.data.posts[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html));
177
+ }
170
178
  }
171
179
  }
172
180
 
@@ -11,10 +11,12 @@ module.exports = {
11
11
  'description',
12
12
  'logo',
13
13
  'icon',
14
+ 'cover_image',
14
15
  'accent_color',
15
16
  'locale',
16
17
  'url',
17
18
  'version',
19
+ 'allow_self_signup',
18
20
  'sentry_dsn',
19
21
  'sentry_env'
20
22
  ])
@@ -1,4 +1,5 @@
1
1
  const path = require('path');
2
+ const errors = require('@tryghost/errors');
2
3
  const urlUtils = require('../../shared/url-utils');
3
4
  const config = require('../../shared/config');
4
5
  const storage = require('../adapters/storage');
@@ -78,5 +79,19 @@ module.exports = {
78
79
  }
79
80
 
80
81
  return urlTransformMap;
82
+ },
83
+
84
+ get htmlToLexicalConverter() {
85
+ try {
86
+ return require('@tryghost/kg-html-to-lexical').htmlToLexical;
87
+ } catch (err) {
88
+ throw new errors.InternalServerError({
89
+ message: 'Unable to convert from source HTML to Lexical',
90
+ context: 'The html-to-lexical package was not installed',
91
+ help: 'Please review any errors from the install process by checking the Ghost logs',
92
+ code: 'HTML_TO_LEXICAL_INSTALLATION',
93
+ err: err
94
+ });
95
+ }
81
96
  }
82
97
  };
@@ -44,6 +44,12 @@ module.exports = function (Bookshelf) {
44
44
  });
45
45
  }
46
46
 
47
+ if (options.page && options.limit) {
48
+ itemCollection
49
+ .query('limit', options.limit)
50
+ .query('offset', options.limit * (options.page - 1));
51
+ }
52
+
47
53
  const result = await itemCollection.fetchAll(options);
48
54
  if (options.withRelated) {
49
55
  _.each(result.models, function each(item) {
@@ -45,7 +45,7 @@ module.exports = function (Bookshelf) {
45
45
  case 'findOne':
46
46
  return baseOptions.concat(extraOptions, ['columns', 'require', 'mongoTransformer']);
47
47
  case 'findAll':
48
- return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer']);
48
+ return baseOptions.concat(extraOptions, ['filter', 'columns', 'mongoTransformer', 'page', 'limit']);
49
49
  case 'findPage':
50
50
  return baseOptions.concat(extraOptions, ['filter', 'order', 'autoOrder', 'page', 'limit', 'columns', 'mongoTransformer']);
51
51
  default:
@@ -552,6 +552,16 @@ Post = ghostBookshelf.Model.extend({
552
552
  let tagsToSave;
553
553
  const ops = [];
554
554
 
555
+ // normally we don't allow both mobiledoc & lexical through at the API level but there's
556
+ // an exception for ?source=html which always sets both when the lexical editor is enabled.
557
+ // That's necessary because at the input serializer layer we don't have access to the
558
+ // actual model to check if this would result in a change of format
559
+ if (this.previous('mobiledoc') && this.get('lexical')) {
560
+ this.set('lexical', null);
561
+ } else if (this.get('mobiledoc') && this.get('lexical')) {
562
+ this.set('mobiledoc', null);
563
+ }
564
+
555
565
  // CASE: disallow published -> scheduled
556
566
  // @TODO: remove when we have versioning based on updated_at
557
567
  if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') {
@@ -653,7 +663,7 @@ Post = ghostBookshelf.Model.extend({
653
663
 
654
664
  // If we're force re-rendering we want to make sure that all image cards
655
665
  // have original dimensions stored in the payload for use by card renderers
656
- if (options.force_rerender) {
666
+ if (options.force_rerender && this.get('mobiledoc')) {
657
667
  this.set('mobiledoc', await mobiledocLib.populateImageSizes(this.get('mobiledoc')));
658
668
  }
659
669
 
@@ -1,12 +1,41 @@
1
1
  const oembedService = require('../oembed');
2
2
 
3
3
  module.exports = class WebmentionMetadata {
4
+ /**
5
+ * Helpers that change the URL for which metadata for a given external resource is fetched. Return undefined to now handle the URL.
6
+ * @type {((url: URL) => URL|undefined)[]}
7
+ */
8
+ #mappers = [];
9
+
10
+ /**
11
+ * @param {(url: URL) => URL|undefined} mapper
12
+ */
13
+ addMapper(mapper) {
14
+ this.#mappers.push(mapper);
15
+ }
16
+
17
+ /**
18
+ *
19
+ * @param {URL} url
20
+ */
21
+ #getMappedUrl(url) {
22
+ for (const mapper of this.#mappers) {
23
+ const mappedUrl = mapper(url);
24
+ if (mappedUrl) {
25
+ return this.#getMappedUrl(mappedUrl);
26
+ }
27
+ }
28
+ return url;
29
+ }
30
+
4
31
  /**
5
32
  * @param {URL} url
6
33
  * @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').WebmentionMetadata>}
7
34
  */
8
35
  async fetch(url) {
9
- const data = await oembedService.fetchOembedDataFromUrl(url.href, 'mention');
36
+ const mappedUrl = this.#getMappedUrl(url);
37
+ const data = await oembedService.fetchOembedDataFromUrl(mappedUrl.href, 'mention');
38
+
10
39
  const result = {
11
40
  siteTitle: data.metadata.publisher,
12
41
  title: data.metadata.title,
@@ -17,6 +46,14 @@ module.exports = class WebmentionMetadata {
17
46
  body: data.body,
18
47
  contentType: data.contentType
19
48
  };
49
+
50
+ if (mappedUrl.href !== url.href) {
51
+ // Still need to fetch body and contentType separately now
52
+ // For verification
53
+ const {body, contentType} = await oembedService.fetchPageHtml(url);
54
+ result.body = body;
55
+ result.contentType = contentType;
56
+ }
20
57
  return result;
21
58
  }
22
59
  };
@@ -28,6 +28,7 @@ module.exports = {
28
28
  /** @type {import('@tryghost/webmentions/lib/MentionsAPI')} */
29
29
  api: null,
30
30
  controller: new MentionController(),
31
+ metadata: new WebmentionMetadata(),
31
32
  /** @type {import('@tryghost/webmentions/lib/MentionSendingService')} */
32
33
  sendingService: null,
33
34
  didInit: false,
@@ -40,7 +41,7 @@ module.exports = {
40
41
  MentionModel: models.Mention,
41
42
  DomainEvents
42
43
  });
43
- const webmentionMetadata = new WebmentionMetadata();
44
+ const webmentionMetadata = this.metadata;
44
45
  const discoveryService = new MentionDiscoveryService({externalRequest});
45
46
  const resourceService = new ResourceService({
46
47
  urlUtils,
@@ -0,0 +1,230 @@
1
+ const {flowRight} = require('lodash');
2
+ const {mapKeyValues, mapQuery} = require('@tryghost/mongo-utils');
3
+ const DomainEvents = require('@tryghost/domain-events');
4
+ const {Offer} = require('@tryghost/members-offers');
5
+ const sentry = require('../../../shared/sentry');
6
+ const logger = require('@tryghost/logging');
7
+
8
+ const statusTransformer = mapKeyValues({
9
+ key: {
10
+ from: 'status',
11
+ to: 'active'
12
+ },
13
+ values: [{
14
+ from: 'active',
15
+ to: true
16
+ }, {
17
+ from: 'archived',
18
+ to: false
19
+ }]
20
+ });
21
+
22
+ const rejectNonStatusTransformer = input => mapQuery(input, function (value, key) {
23
+ if (key !== 'status') {
24
+ return;
25
+ }
26
+
27
+ return {
28
+ [key]: value
29
+ };
30
+ });
31
+
32
+ const mongoTransformer = flowRight(statusTransformer, rejectNonStatusTransformer);
33
+
34
+ /**
35
+ * @typedef {object} BaseOptions
36
+ * @prop {import('knex').Transaction} transacting
37
+ */
38
+
39
+ /**
40
+ * @typedef {object} ListOptions
41
+ * @prop {import('knex').Transaction} transacting
42
+ * @prop {string} filter
43
+ */
44
+
45
+ class OfferBookshelfRepository {
46
+ /**
47
+ * @param {{forge: (data: object) => import('bookshelf').Model<Offer.OfferProps>}} OfferModel
48
+ * @param {{forge: (data: object) => import('bookshelf').Model<any>}} OfferRedemptionModel
49
+ */
50
+ constructor(OfferModel, OfferRedemptionModel) {
51
+ /** @private */
52
+ this.OfferModel = OfferModel;
53
+ /** @private */
54
+ this.OfferRedemptionModel = OfferRedemptionModel;
55
+ }
56
+
57
+ /**
58
+ * @template T
59
+ * @param {(t: import('knex').Transaction) => Promise<T>} cb
60
+ * @returns {Promise<T>}
61
+ */
62
+ async createTransaction(cb) {
63
+ return this.OfferModel.transaction(cb);
64
+ }
65
+
66
+ /**
67
+ * @param {string} name
68
+ * @param {BaseOptions} [options]
69
+ * @returns {Promise<boolean>}
70
+ */
71
+ async existsByName(name, options) {
72
+ const model = await this.OfferModel.findOne({name}, options);
73
+ if (!model) {
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * @param {string} code
81
+ * @param {BaseOptions} [options]
82
+ * @returns {Promise<boolean>}
83
+ */
84
+ async existsByCode(code, options) {
85
+ const model = await this.OfferModel.findOne({code}, options);
86
+ if (!model) {
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+
92
+ /**
93
+ * @private
94
+ * @param {import('bookshelf').Model<any>} model
95
+ * @param {BaseOptions} options
96
+ * @returns {Promise<import('@tryghost/members-offers').Offer>}
97
+ */
98
+ async mapToOffer(model, options) {
99
+ const json = model.toJSON();
100
+
101
+ const count = await this.OfferRedemptionModel.where({offer_id: json.id}).count('id', {
102
+ transacting: options.transacting
103
+ });
104
+ try {
105
+ return await Offer.create({
106
+ id: json.id,
107
+ name: json.name,
108
+ code: json.code,
109
+ display_title: json.portal_title,
110
+ display_description: json.portal_description,
111
+ type: json.discount_type === 'amount' ? 'fixed' : json.discount_type,
112
+ amount: json.discount_amount,
113
+ cadence: json.interval,
114
+ currency: json.currency,
115
+ duration: json.duration,
116
+ duration_in_months: json.duration_in_months,
117
+ redemptionCount: count,
118
+ status: json.active ? 'active' : 'archived',
119
+ tier: {
120
+ id: json.product.id,
121
+ name: json.product.name
122
+ }
123
+ }, null);
124
+ } catch (err) {
125
+ logger.error(err);
126
+ sentry.captureException(err);
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @param {string} id
133
+ * @param {BaseOptions} [options]
134
+ * @returns {Promise<import('@tryghost/members-offers').Offer>}
135
+ */
136
+ async getById(id, options) {
137
+ const model = await this.OfferModel.findOne({id}, {
138
+ ...options,
139
+ withRelated: ['product']
140
+ });
141
+
142
+ if (!model) {
143
+ return null;
144
+ }
145
+
146
+ return this.mapToOffer(model, options);
147
+ }
148
+
149
+ /**
150
+ * @param {string} id stripe_coupon_id
151
+ * @param {BaseOptions} [options]
152
+ * @returns {Promise<import('@tryghost/members-offers').Offer>}
153
+ */
154
+ async getByStripeCouponId(id, options) {
155
+ const model = await this.OfferModel.findOne({stripe_coupon_id: id}, {
156
+ ...options,
157
+ withRelated: ['product']
158
+ });
159
+
160
+ if (!model) {
161
+ return null;
162
+ }
163
+
164
+ return this.mapToOffer(model, options);
165
+ }
166
+
167
+ /**
168
+ * @param {ListOptions} options
169
+ * @returns {Promise<import('@tryghost/members-offers').Offer[]>}
170
+ */
171
+ async getAll(options) {
172
+ const models = await this.OfferModel.findAll({
173
+ ...options,
174
+ mongoTransformer,
175
+ withRelated: ['product']
176
+ });
177
+
178
+ const mapOptions = {
179
+ transacting: options && options.transacting
180
+ };
181
+
182
+ const offers = models.map(model => this.mapToOffer(model, mapOptions));
183
+
184
+ return (await Promise.all(offers)).filter(offer => offer !== null);
185
+ }
186
+
187
+ /**
188
+ * @param {import('@tryghost/members-offers').Offer} offer
189
+ * @param {BaseOptions} [options]
190
+ * @returns {Promise<void>}
191
+ */
192
+ async save(offer, options) {
193
+ /** @type any */
194
+ const data = {
195
+ id: offer.id,
196
+ name: offer.name.value,
197
+ code: offer.code.value,
198
+ portal_title: offer.displayTitle.value || null,
199
+ portal_description: offer.displayDescription.value || null,
200
+ discount_type: offer.type.value === 'fixed' ? 'amount' : offer.type.value,
201
+ discount_amount: offer.amount.value,
202
+ interval: offer.cadence.value,
203
+ product_id: offer.tier.id,
204
+ duration: offer.duration.value.type,
205
+ duration_in_months: offer.duration.value.type === 'repeating' ? offer.duration.value.months : null,
206
+ currency: offer.currency ? offer.currency.value : null,
207
+ active: offer.status.value === 'active'
208
+ };
209
+
210
+ if (offer.isNew) {
211
+ await this.OfferModel.add(data, options);
212
+ } else {
213
+ await this.OfferModel.edit(data, {...options, id: data.id});
214
+ }
215
+
216
+ for (const event of offer.events) {
217
+ if (options.transacting) {
218
+ // Only dispatch the event after the transaction has finished
219
+ // Because else the offer won't be committed to the database yet
220
+ options.transacting.executionPromise.then(() => {
221
+ DomainEvents.dispatch(event);
222
+ });
223
+ } else {
224
+ DomainEvents.dispatch(event);
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ module.exports = OfferBookshelfRepository;
@@ -4,6 +4,7 @@ const OffersModule = require('@tryghost/members-offers');
4
4
  const config = require('../../../shared/config');
5
5
  const urlUtils = require('../../../shared/url-utils');
6
6
  const models = require('../../models');
7
+ const OfferBookshelfRepository = require('./OfferBookshelfRepository');
7
8
 
8
9
  let redirectManager;
9
10
 
@@ -15,10 +16,13 @@ module.exports = {
15
16
  return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
16
17
  }
17
18
  });
19
+ const repository = new OfferBookshelfRepository(
20
+ models.Offer,
21
+ models.OfferRedemption
22
+ );
18
23
  const offersModule = OffersModule.create({
19
- OfferModel: models.Offer,
20
- OfferRedemptionModel: models.OfferRedemption,
21
- redirectManager
24
+ redirectManager,
25
+ repository
22
26
  });
23
27
 
24
28
  this.api = offersModule.api;
@@ -9,10 +9,12 @@ module.exports = function getSiteProperties() {
9
9
  description: settingsCache.get('description'),
10
10
  logo: settingsCache.get('logo'),
11
11
  icon: settingsCache.get('icon'),
12
+ cover_image: settingsCache.get('cover_image'),
12
13
  accent_color: settingsCache.get('accent_color'),
13
14
  locale: settingsCache.get('locale'),
14
15
  url: urlUtils.urlFor('home', true),
15
- version: ghostVersion.safe
16
+ version: ghostVersion.safe,
17
+ allow_self_signup: settingsCache.get('allow_self_signup')
16
18
  };
17
19
 
18
20
  if (config.get('client_sentry') && !config.get('client_sentry').disabled) {
@@ -0,0 +1,28 @@
1
+ module.exports = class RecommendationEnablerService {
2
+ /** @type {import('../settings/SettingsBREADService')} */
3
+ #settingsService;
4
+
5
+ /**
6
+ * @param {object} deps
7
+ * @param {import('../settings/SettingsBREADService')} deps.settingsService
8
+ */
9
+ constructor(deps) {
10
+ this.#settingsService = deps.settingsService;
11
+ }
12
+
13
+ /**
14
+ * @returns {string}
15
+ */
16
+ getSetting() {
17
+ this.#settingsService.read('recommendations_enabled');
18
+ }
19
+
20
+ /**
21
+ *
22
+ * @param {string} value
23
+ * @returns Promise<void>
24
+ */
25
+ async setSetting(value) {
26
+ this.#settingsService.edit([{key: 'recommendations_enabled', value}], {context: {internal: true}});
27
+ }
28
+ };
@@ -23,7 +23,14 @@ class RecommendationServiceWrapper {
23
23
  const urlUtils = require('../../../shared/url-utils');
24
24
  const models = require('../../models');
25
25
  const sentry = require('../../../shared/sentry');
26
- const {BookshelfRecommendationRepository, RecommendationService, RecommendationController, WellknownService} = require('@tryghost/recommendations');
26
+ const settings = require('../settings');
27
+ const RecommendationEnablerService = require('./RecommendationEnablerService');
28
+ const {
29
+ BookshelfRecommendationRepository,
30
+ RecommendationService,
31
+ RecommendationController,
32
+ WellknownService
33
+ } = require('@tryghost/recommendations');
27
34
 
28
35
  const mentions = require('../mentions');
29
36
 
@@ -37,11 +44,15 @@ class RecommendationServiceWrapper {
37
44
  urlUtils
38
45
  });
39
46
 
47
+ const settingsService = settings.getSettingsBREADServiceInstance();
48
+ const recommendationEnablerService = new RecommendationEnablerService({settingsService});
49
+
40
50
  this.repository = new BookshelfRecommendationRepository(models.Recommendation, {
41
51
  sentry
42
52
  });
43
53
  this.service = new RecommendationService({
44
54
  repository: this.repository,
55
+ recommendationEnablerService,
45
56
  wellknownService,
46
57
  mentionSendingService: mentions.sendingService
47
58
  });
@@ -51,6 +62,17 @@ class RecommendationServiceWrapper {
51
62
 
52
63
  // eslint-disable-next-line no-console
53
64
  this.service.init().catch(console.error);
65
+
66
+ // Add mapper to WebmentionMetadata
67
+ mentions.metadata.addMapper((url) => {
68
+ const p = '/.well-known/recommendations.json';
69
+ if (url.pathname.endsWith(p)) {
70
+ // Strip p
71
+ const newUrl = new URL(url.toString());
72
+ newUrl.pathname = newUrl.pathname.slice(0, -p.length);
73
+ return newUrl;
74
+ }
75
+ });
54
76
  }
55
77
  }
56
78
 
@@ -147,7 +147,7 @@ class SettingsBREADService {
147
147
  * @param {Object[]} settings
148
148
  * @param {Object} options
149
149
  * @param {Object} [options.context]
150
- * @param {Object} [stripeConnectData]
150
+ * @param {Object|null} [stripeConnectData]
151
151
  * @returns
152
152
  */
153
153
  async edit(settings, options, stripeConnectData) {