ghost 5.61.3 → 5.63.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 (182) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.63.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.63.0.tgz +0 -0
  3. package/components/{tryghost-adapter-manager-5.61.3.tgz → tryghost-adapter-manager-5.63.0.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.63.0.tgz +0 -0
  5. package/components/tryghost-api-framework-5.63.0.tgz +0 -0
  6. package/components/{tryghost-api-version-compatibility-service-5.61.3.tgz → tryghost-api-version-compatibility-service-5.63.0.tgz} +0 -0
  7. package/components/{tryghost-audience-feedback-5.61.3.tgz → tryghost-audience-feedback-5.63.0.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.63.0.tgz +0 -0
  9. package/components/tryghost-bootstrap-socket-5.63.0.tgz +0 -0
  10. package/components/tryghost-collections-5.63.0.tgz +0 -0
  11. package/components/{tryghost-constants-5.61.3.tgz → tryghost-constants-5.63.0.tgz} +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.63.0.tgz +0 -0
  13. package/components/tryghost-data-generator-5.63.0.tgz +0 -0
  14. package/components/tryghost-domain-events-5.63.0.tgz +0 -0
  15. package/components/tryghost-donations-5.63.0.tgz +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.63.0.tgz +0 -0
  17. package/components/tryghost-email-analytics-provider-mailgun-5.63.0.tgz +0 -0
  18. package/components/{tryghost-email-analytics-service-5.61.3.tgz → tryghost-email-analytics-service-5.63.0.tgz} +0 -0
  19. package/components/tryghost-email-content-generator-5.63.0.tgz +0 -0
  20. package/components/tryghost-email-events-5.63.0.tgz +0 -0
  21. package/components/{tryghost-email-service-5.61.3.tgz → tryghost-email-service-5.63.0.tgz} +0 -0
  22. package/components/tryghost-email-suppression-list-5.63.0.tgz +0 -0
  23. package/components/tryghost-event-aware-cache-wrapper-5.63.0.tgz +0 -0
  24. package/components/tryghost-express-dynamic-redirects-5.63.0.tgz +0 -0
  25. package/components/{tryghost-external-media-inliner-5.61.3.tgz → tryghost-external-media-inliner-5.63.0.tgz} +0 -0
  26. package/components/{tryghost-extract-api-key-5.61.3.tgz → tryghost-extract-api-key-5.63.0.tgz} +0 -0
  27. package/components/{tryghost-html-to-plaintext-5.61.3.tgz → tryghost-html-to-plaintext-5.63.0.tgz} +0 -0
  28. package/components/tryghost-i18n-5.63.0.tgz +0 -0
  29. package/components/{tryghost-importer-handler-content-files-5.61.3.tgz → tryghost-importer-handler-content-files-5.63.0.tgz} +0 -0
  30. package/components/{tryghost-importer-revue-5.61.3.tgz → tryghost-importer-revue-5.63.0.tgz} +0 -0
  31. package/components/tryghost-in-memory-repository-5.63.0.tgz +0 -0
  32. package/components/{tryghost-job-manager-5.61.3.tgz → tryghost-job-manager-5.63.0.tgz} +0 -0
  33. package/components/tryghost-link-redirects-5.63.0.tgz +0 -0
  34. package/components/tryghost-link-replacer-5.63.0.tgz +0 -0
  35. package/components/{tryghost-link-tracking-5.61.3.tgz → tryghost-link-tracking-5.63.0.tgz} +0 -0
  36. package/components/tryghost-magic-link-5.63.0.tgz +0 -0
  37. package/components/tryghost-mail-events-5.63.0.tgz +0 -0
  38. package/components/tryghost-mailgun-client-5.63.0.tgz +0 -0
  39. package/components/{tryghost-member-attribution-5.61.3.tgz → tryghost-member-attribution-5.63.0.tgz} +0 -0
  40. package/components/tryghost-member-events-5.63.0.tgz +0 -0
  41. package/components/tryghost-members-api-5.63.0.tgz +0 -0
  42. package/components/{tryghost-members-csv-5.61.3.tgz → tryghost-members-csv-5.63.0.tgz} +0 -0
  43. package/components/{tryghost-members-events-service-5.61.3.tgz → tryghost-members-events-service-5.63.0.tgz} +0 -0
  44. package/components/{tryghost-members-importer-5.61.3.tgz → tryghost-members-importer-5.63.0.tgz} +0 -0
  45. package/components/tryghost-members-offers-5.63.0.tgz +0 -0
  46. package/components/tryghost-members-payments-5.63.0.tgz +0 -0
  47. package/components/tryghost-members-ssr-5.63.0.tgz +0 -0
  48. package/components/{tryghost-members-stripe-service-5.61.3.tgz → tryghost-members-stripe-service-5.63.0.tgz} +0 -0
  49. package/components/tryghost-mentions-email-report-5.63.0.tgz +0 -0
  50. package/components/{tryghost-milestones-5.61.3.tgz → tryghost-milestones-5.63.0.tgz} +0 -0
  51. package/components/{tryghost-minifier-5.61.3.tgz → tryghost-minifier-5.63.0.tgz} +0 -0
  52. package/components/tryghost-model-to-domain-event-interceptor-5.63.0.tgz +0 -0
  53. package/components/{tryghost-mw-api-version-mismatch-5.61.3.tgz → tryghost-mw-api-version-mismatch-5.63.0.tgz} +0 -0
  54. package/components/tryghost-mw-cache-control-5.63.0.tgz +0 -0
  55. package/components/{tryghost-mw-error-handler-5.61.3.tgz → tryghost-mw-error-handler-5.63.0.tgz} +0 -0
  56. package/components/tryghost-mw-session-from-token-5.63.0.tgz +0 -0
  57. package/components/tryghost-mw-update-user-last-seen-5.63.0.tgz +0 -0
  58. package/components/tryghost-mw-version-match-5.63.0.tgz +0 -0
  59. package/components/tryghost-mw-vhost-5.63.0.tgz +0 -0
  60. package/components/tryghost-nql-filter-expansions-5.63.0.tgz +0 -0
  61. package/components/tryghost-oembed-service-5.63.0.tgz +0 -0
  62. package/components/{tryghost-package-json-5.61.3.tgz → tryghost-package-json-5.63.0.tgz} +0 -0
  63. package/components/tryghost-post-events-5.63.0.tgz +0 -0
  64. package/components/tryghost-post-revisions-5.63.0.tgz +0 -0
  65. package/components/tryghost-posts-service-5.63.0.tgz +0 -0
  66. package/components/tryghost-recommendations-5.63.0.tgz +0 -0
  67. package/components/tryghost-referrers-5.63.0.tgz +0 -0
  68. package/components/{tryghost-security-5.61.3.tgz → tryghost-security-5.63.0.tgz} +0 -0
  69. package/components/tryghost-session-service-5.63.0.tgz +0 -0
  70. package/components/tryghost-settings-path-manager-5.63.0.tgz +0 -0
  71. package/components/tryghost-slack-notifications-5.63.0.tgz +0 -0
  72. package/components/{tryghost-staff-service-5.61.3.tgz → tryghost-staff-service-5.63.0.tgz} +0 -0
  73. package/components/tryghost-stats-service-5.63.0.tgz +0 -0
  74. package/components/{tryghost-tiers-5.61.3.tgz → tryghost-tiers-5.63.0.tgz} +0 -0
  75. package/components/{tryghost-update-check-service-5.61.3.tgz → tryghost-update-check-service-5.63.0.tgz} +0 -0
  76. package/components/{tryghost-verification-trigger-5.61.3.tgz → tryghost-verification-trigger-5.63.0.tgz} +0 -0
  77. package/components/tryghost-version-notifications-data-service-5.63.0.tgz +0 -0
  78. package/components/tryghost-webmentions-5.63.0.tgz +0 -0
  79. package/core/built/admin/assets/{chunk.143.da84b3b434141aff0f01.js → chunk.143.1c5d21facf1f9b9beef9.js} +6 -5
  80. package/core/built/admin/assets/{chunk.178.659d369f286d4162b294.js → chunk.178.43b03c9ac011a54262b0.js} +4 -4
  81. package/core/built/admin/assets/{chunk.208.dbf172ad32f72f21a5dc.js → chunk.237.9b7032162949850f6c76.js} +778 -702
  82. package/core/built/admin/assets/{ghost-8a4e981c272f793157133814ca7c7e84.css → ghost-33664cad4cd6664a8b5fa56e62c5005f.css} +1 -1
  83. package/core/built/admin/assets/{ghost-54f84395df6a6fb47b37008ded8eba22.js → ghost-7c3f2de2ec83e591ad9f9db5165b2733.js} +68 -63
  84. package/core/built/admin/assets/ghost-dark-0452daeaee3a9b16dcd954ea60dad518.css +1 -0
  85. package/core/built/admin/assets/{vendor-3631184082d609038638c1e169a002e7.js → vendor-f8ce8bd43cf5dad6608f828ab48cee9b.js} +249 -322
  86. package/core/built/admin/index.html +6 -6
  87. package/core/frontend/helpers/foreach.js +8 -0
  88. package/core/frontend/helpers/get.js +3 -0
  89. package/core/frontend/meta/schema.js +1 -1
  90. package/core/frontend/services/data/checks.js +6 -0
  91. package/core/frontend/src/cards/css/collection.css +186 -0
  92. package/core/frontend/src/cards/css/header_v2.css +19 -23
  93. package/core/frontend/src/cards/css/signup.css +20 -23
  94. package/core/server/adapters/storage/LocalStorageBase.js +1 -8
  95. package/core/server/api/endpoints/recommendations-public.js +44 -5
  96. package/core/server/api/endpoints/recommendations.js +7 -3
  97. package/core/server/api/endpoints/utils/serializers/input/pages.js +8 -0
  98. package/core/server/api/endpoints/utils/serializers/input/posts.js +9 -1
  99. package/core/server/api/endpoints/utils/serializers/output/members.js +22 -5
  100. package/core/server/api/endpoints/utils/serializers/output/site.js +2 -0
  101. package/core/server/data/exporter/table-lists.js +3 -1
  102. package/core/server/data/migrations/versions/5.63/2023-09-12-11-22-10-add-recommendation-click-events-table.js +8 -0
  103. package/core/server/data/migrations/versions/5.63/2023-09-12-11-22-11-add-recommendation-subscribe-events-table.js +8 -0
  104. package/core/server/data/migrations/versions/5.63/2023-09-13-13-03-10-add-ghost-core-content-integration.js +57 -0
  105. package/core/server/data/migrations/versions/5.63/2023-09-13-13-34-11-add-ghost-core-content-integration-key.js +87 -0
  106. package/core/server/data/schema/fixtures/fixtures.json +7 -0
  107. package/core/server/data/schema/schema.js +12 -0
  108. package/core/server/lib/lexical.js +35 -1
  109. package/core/server/models/post.js +12 -2
  110. package/core/server/models/recommendation-click-event.js +22 -0
  111. package/core/server/models/recommendation-subscribe-event.js +22 -0
  112. package/core/server/services/collections/BookshelfCollectionsRepository.js +67 -5
  113. package/core/server/services/collections/service.js +8 -8
  114. package/core/server/services/members/middleware.js +14 -1
  115. package/core/server/services/members/utils.js +1 -0
  116. package/core/server/services/mentions/WebmentionMetadata.js +38 -1
  117. package/core/server/services/mentions/service.js +2 -1
  118. package/core/server/services/offers/OfferBookshelfRepository.js +230 -0
  119. package/core/server/services/offers/service.js +7 -3
  120. package/core/server/services/public-config/site.js +3 -1
  121. package/core/server/services/recommendations/RecommendationEnablerService.js +28 -0
  122. package/core/server/services/recommendations/RecommendationServiceWrapper.js +45 -2
  123. package/core/server/services/settings/SettingsBREADService.js +6 -1
  124. package/core/server/services/settings/settings-service.js +1 -0
  125. package/core/server/services/settings-helpers/SettingsHelpers.js +7 -0
  126. package/core/server/web/members/app.js +21 -0
  127. package/core/shared/config/defaults.json +2 -2
  128. package/core/shared/settings-cache/public.js +3 -1
  129. package/package.json +161 -160
  130. package/yarn.lock +492 -512
  131. package/components/tryghost-adapter-cache-memory-ttl-5.61.3.tgz +0 -0
  132. package/components/tryghost-adapter-cache-redis-5.61.3.tgz +0 -0
  133. package/components/tryghost-announcement-bar-settings-5.61.3.tgz +0 -0
  134. package/components/tryghost-api-framework-5.61.3.tgz +0 -0
  135. package/components/tryghost-bookshelf-repository-5.61.3.tgz +0 -0
  136. package/components/tryghost-bootstrap-socket-5.61.3.tgz +0 -0
  137. package/components/tryghost-collections-5.61.3.tgz +0 -0
  138. package/components/tryghost-custom-theme-settings-service-5.61.3.tgz +0 -0
  139. package/components/tryghost-data-generator-5.61.3.tgz +0 -0
  140. package/components/tryghost-domain-events-5.61.3.tgz +0 -0
  141. package/components/tryghost-donations-5.61.3.tgz +0 -0
  142. package/components/tryghost-dynamic-routing-events-5.61.3.tgz +0 -0
  143. package/components/tryghost-email-analytics-provider-mailgun-5.61.3.tgz +0 -0
  144. package/components/tryghost-email-content-generator-5.61.3.tgz +0 -0
  145. package/components/tryghost-email-events-5.61.3.tgz +0 -0
  146. package/components/tryghost-email-suppression-list-5.61.3.tgz +0 -0
  147. package/components/tryghost-event-aware-cache-wrapper-5.61.3.tgz +0 -0
  148. package/components/tryghost-express-dynamic-redirects-5.61.3.tgz +0 -0
  149. package/components/tryghost-i18n-5.61.3.tgz +0 -0
  150. package/components/tryghost-in-memory-repository-5.61.3.tgz +0 -0
  151. package/components/tryghost-link-redirects-5.61.3.tgz +0 -0
  152. package/components/tryghost-link-replacer-5.61.3.tgz +0 -0
  153. package/components/tryghost-magic-link-5.61.3.tgz +0 -0
  154. package/components/tryghost-mail-events-5.61.3.tgz +0 -0
  155. package/components/tryghost-mailgun-client-5.61.3.tgz +0 -0
  156. package/components/tryghost-member-events-5.61.3.tgz +0 -0
  157. package/components/tryghost-members-api-5.61.3.tgz +0 -0
  158. package/components/tryghost-members-offers-5.61.3.tgz +0 -0
  159. package/components/tryghost-members-payments-5.61.3.tgz +0 -0
  160. package/components/tryghost-members-ssr-5.61.3.tgz +0 -0
  161. package/components/tryghost-mentions-email-report-5.61.3.tgz +0 -0
  162. package/components/tryghost-model-to-domain-event-interceptor-5.61.3.tgz +0 -0
  163. package/components/tryghost-mw-cache-control-5.61.3.tgz +0 -0
  164. package/components/tryghost-mw-session-from-token-5.61.3.tgz +0 -0
  165. package/components/tryghost-mw-update-user-last-seen-5.61.3.tgz +0 -0
  166. package/components/tryghost-mw-version-match-5.61.3.tgz +0 -0
  167. package/components/tryghost-mw-vhost-5.61.3.tgz +0 -0
  168. package/components/tryghost-nql-filter-expansions-5.61.3.tgz +0 -0
  169. package/components/tryghost-oembed-service-5.61.3.tgz +0 -0
  170. package/components/tryghost-post-events-5.61.3.tgz +0 -0
  171. package/components/tryghost-post-revisions-5.61.3.tgz +0 -0
  172. package/components/tryghost-posts-service-5.61.3.tgz +0 -0
  173. package/components/tryghost-recommendations-5.61.3.tgz +0 -0
  174. package/components/tryghost-referrers-5.61.3.tgz +0 -0
  175. package/components/tryghost-session-service-5.61.3.tgz +0 -0
  176. package/components/tryghost-settings-path-manager-5.61.3.tgz +0 -0
  177. package/components/tryghost-slack-notifications-5.61.3.tgz +0 -0
  178. package/components/tryghost-stats-service-5.61.3.tgz +0 -0
  179. package/components/tryghost-version-notifications-data-service-5.61.3.tgz +0 -0
  180. package/components/tryghost-webmentions-5.61.3.tgz +0 -0
  181. package/core/built/admin/assets/ghost-dark-084169b0e968ef763dfbbf63b253e0c6.css +0 -1
  182. /package/core/built/admin/assets/{chunk.208.dbf172ad32f72f21a5dc.js.LICENSE.txt → chunk.237.9b7032162949850f6c76.js.LICENSE.txt} +0 -0
@@ -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
+ };
@@ -4,6 +4,16 @@ class RecommendationServiceWrapper {
4
4
  */
5
5
  repository;
6
6
 
7
+ /**
8
+ * @type {import('@tryghost/recommendations').BookshelfClickEventRepository}
9
+ */
10
+ clickEventRepository;
11
+
12
+ /**
13
+ * @type {import('@tryghost/recommendations').BookshelfSubscribeEventRepository}
14
+ */
15
+ subscribeEventRepository;
16
+
7
17
  /**
8
18
  * @type {import('@tryghost/recommendations').RecommendationController}
9
19
  */
@@ -23,7 +33,15 @@ class RecommendationServiceWrapper {
23
33
  const urlUtils = require('../../../shared/url-utils');
24
34
  const models = require('../../models');
25
35
  const sentry = require('../../../shared/sentry');
26
- const {BookshelfRecommendationRepository, RecommendationService, RecommendationController, WellknownService} = require('@tryghost/recommendations');
36
+ const settings = require('../settings');
37
+ const RecommendationEnablerService = require('./RecommendationEnablerService');
38
+ const {
39
+ BookshelfRecommendationRepository,
40
+ RecommendationService,
41
+ RecommendationController,
42
+ WellknownService,
43
+ BookshelfClickEventRepository
44
+ } = require('@tryghost/recommendations');
27
45
 
28
46
  const mentions = require('../mentions');
29
47
 
@@ -37,13 +55,27 @@ class RecommendationServiceWrapper {
37
55
  urlUtils
38
56
  });
39
57
 
58
+ const settingsService = settings.getSettingsBREADServiceInstance();
59
+ const recommendationEnablerService = new RecommendationEnablerService({settingsService});
60
+
40
61
  this.repository = new BookshelfRecommendationRepository(models.Recommendation, {
41
62
  sentry
42
63
  });
64
+
65
+ this.clickEventRepository = new BookshelfClickEventRepository(models.RecommendationClickEvent, {
66
+ sentry
67
+ });
68
+ this.subscribeEventRepository = new BookshelfClickEventRepository(models.RecommendationSubscribeEvent, {
69
+ sentry
70
+ });
71
+
43
72
  this.service = new RecommendationService({
44
73
  repository: this.repository,
74
+ recommendationEnablerService,
45
75
  wellknownService,
46
- mentionSendingService: mentions.sendingService
76
+ mentionSendingService: mentions.sendingService,
77
+ clickEventRepository: this.clickEventRepository,
78
+ subscribeEventRepository: this.subscribeEventRepository
47
79
  });
48
80
  this.controller = new RecommendationController({
49
81
  service: this.service
@@ -51,6 +83,17 @@ class RecommendationServiceWrapper {
51
83
 
52
84
  // eslint-disable-next-line no-console
53
85
  this.service.init().catch(console.error);
86
+
87
+ // Add mapper to WebmentionMetadata
88
+ mentions.metadata.addMapper((url) => {
89
+ const p = '/.well-known/recommendations.json';
90
+ if (url.pathname.endsWith(p)) {
91
+ // Strip p
92
+ const newUrl = new URL(url.toString());
93
+ newUrl.pathname = newUrl.pathname.slice(0, -p.length);
94
+ return newUrl;
95
+ }
96
+ });
54
97
  }
55
98
  }
56
99
 
@@ -66,6 +66,11 @@ class SettingsBREADService {
66
66
  const adminUrl = urlUtils.urlFor('admin', true);
67
67
  const signinURL = new URL(adminUrl);
68
68
  signinURL.hash = `/settings/members/?verifyEmail=${token}`;
69
+ // NOTE: to be removed in future, this is to ensure that the new settings are used when enabled
70
+ if (labsService && labsService.isSet('adminXSettings')) {
71
+ signinURL.hash = `/settings-x/portal/edit?verifyEmail=${token}`;
72
+ }
73
+
69
74
  return signinURL.href;
70
75
  }
71
76
  };
@@ -147,7 +152,7 @@ class SettingsBREADService {
147
152
  * @param {Object[]} settings
148
153
  * @param {Object} options
149
154
  * @param {Object} [options.context]
150
- * @param {Object} [stripeConnectData]
155
+ * @param {Object|null} [stripeConnectData]
151
156
  * @returns
152
157
  */
153
158
  async edit(settings, options, stripeConnectData) {
@@ -87,6 +87,7 @@ module.exports = {
87
87
 
88
88
  fields.push(new CalculatedField({key: 'members_enabled', type: 'boolean', group: 'members', fn: settingsHelpers.isMembersEnabled.bind(settingsHelpers), dependents: ['members_signup_access']}));
89
89
  fields.push(new CalculatedField({key: 'members_invite_only', type: 'boolean', group: 'members', fn: settingsHelpers.isMembersInviteOnly.bind(settingsHelpers), dependents: ['members_signup_access']}));
90
+ fields.push(new CalculatedField({key: 'allow_self_signup', type: 'boolean', group: 'members', fn: settingsHelpers.allowSelfSignup.bind(settingsHelpers), dependents: ['members_signup_access', 'portal_plans', 'stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']}));
90
91
  fields.push(new CalculatedField({key: 'paid_members_enabled', type: 'boolean', group: 'members', fn: settingsHelpers.arePaidMembersEnabled.bind(settingsHelpers), dependents: ['members_signup_access', 'stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']}));
91
92
  fields.push(new CalculatedField({key: 'firstpromoter_account', type: 'string', group: 'firstpromoter', fn: settingsHelpers.getFirstpromoterId.bind(settingsHelpers), dependents: ['firstpromoter', 'firstpromoter_id']}));
92
93
  fields.push(new CalculatedField({key: 'donations_enabled', type: 'boolean', group: 'donations', fn: settingsHelpers.areDonationsEnabled.bind(settingsHelpers), dependents: ['stripe_secret_key', 'stripe_publishable_key', 'stripe_connect_secret_key', 'stripe_connect_publishable_key']}));
@@ -20,6 +20,13 @@ class SettingsHelpers {
20
20
  return this.settingsCache.get('members_signup_access') === 'invite';
21
21
  }
22
22
 
23
+ /**
24
+ * NOTE! The backend still allows to self signup if this returns false because a site might use built-in free signup forms apart from Portal
25
+ */
26
+ allowSelfSignup() {
27
+ return this.settingsCache.get('members_signup_access') === 'all' && (this.settingsCache.get('portal_plans').includes('free') || !this.arePaidMembersEnabled());
28
+ }
29
+
23
30
  /**
24
31
  * @param {'direct' | 'connect'} type - The "type" of keys to fetch from settings
25
32
  * @returns {{publicKey: string, secretKey: string} | null}
@@ -88,6 +88,27 @@ module.exports = function setupMembersApp() {
88
88
  announcementRouter()
89
89
  );
90
90
 
91
+ // Recommendations
92
+ membersApp.post(
93
+ '/api/recommendations/:id/clicked',
94
+ middleware.loadMemberSession,
95
+ http(api.recommendationsPublic.trackClicked)
96
+ );
97
+
98
+ // Recommendations
99
+ membersApp.post(
100
+ '/api/recommendations/:id/subscribed',
101
+ middleware.loadMemberSession,
102
+ http(api.recommendationsPublic.trackSubscribed)
103
+ );
104
+
105
+ // Allow external systems to read public settings via the members api
106
+ // Without CORS issues and without a required integration token
107
+ // 1. Detect if a site is Running Ghost
108
+ // 2. For recommendations to know when we can offer 'one-click-subscribe' to know if members are enabled
109
+ // Why not content API? Domain can be different from recommended domain + CORS issues
110
+ membersApp.get('/api/site', http(api.site.read));
111
+
91
112
  // API error handling
92
113
  membersApp.use('/api', errorHandler.resourceNotFound);
93
114
  membersApp.use('/api', errorHandler.handleJSONResponse(sentry));
@@ -182,7 +182,7 @@
182
182
  },
183
183
  "portal": {
184
184
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
185
- "version": "2.34"
185
+ "version": "2.36"
186
186
  },
187
187
  "sodoSearch": {
188
188
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -202,7 +202,7 @@
202
202
  "version": "0.4"
203
203
  },
204
204
  "adminX": {
205
- "url": "https://cdn.jsdelivr.net/ghost/admin-x-settings@~{version}/dist/admin-x-settings.umd.js",
205
+ "url": "https://cdn.jsdelivr.net/ghost/admin-x-settings@~{version}/dist/admin-x-settings.js",
206
206
  "version": "0.0"
207
207
  },
208
208
  "signupForm": {
@@ -28,6 +28,7 @@ module.exports = {
28
28
  twitter_description: 'twitter_description',
29
29
  members_support_address: 'members_support_address',
30
30
  members_enabled: 'members_enabled',
31
+ allow_self_signup: 'allow_self_signup',
31
32
  members_invite_only: 'members_invite_only',
32
33
  paid_members_enabled: 'paid_members_enabled',
33
34
  firstpromoter_account: 'firstpromoter_account',
@@ -40,5 +41,6 @@ module.exports = {
40
41
  portal_name: 'portal_name',
41
42
  portal_button: 'portal_button',
42
43
  comments_enabled: 'comments_enabled',
43
- recommendations_enabled: 'recommendations_enabled'
44
+ recommendations_enabled: 'recommendations_enabled',
45
+ outbound_link_tagging: 'outbound_link_tagging'
44
46
  };