ghost 5.115.0 → 5.115.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  2. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  3. package/components/{tryghost-announcement-bar-settings-5.115.0.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
  4. package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.115.1.tgz +0 -0
  6. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.115.0.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
  9. package/components/tryghost-domain-events-5.115.1.tgz +0 -0
  10. package/components/tryghost-donations-5.115.1.tgz +0 -0
  11. package/components/{tryghost-email-addresses-5.115.0.tgz → tryghost-email-addresses-5.115.1.tgz} +0 -0
  12. package/components/{tryghost-email-content-generator-5.115.0.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
  13. package/components/{tryghost-email-events-5.115.0.tgz → tryghost-email-events-5.115.1.tgz} +0 -0
  14. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  15. package/components/{tryghost-email-suppression-list-5.115.0.tgz → tryghost-email-suppression-list-5.115.1.tgz} +0 -0
  16. package/components/{tryghost-express-dynamic-redirects-5.115.0.tgz → tryghost-express-dynamic-redirects-5.115.1.tgz} +0 -0
  17. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  18. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  19. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  20. package/components/{tryghost-importer-handler-content-files-5.115.0.tgz → tryghost-importer-handler-content-files-5.115.1.tgz} +0 -0
  21. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  22. package/components/{tryghost-job-manager-5.115.0.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
  23. package/components/{tryghost-link-redirects-5.115.0.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
  24. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  25. package/components/{tryghost-magic-link-5.115.0.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
  26. package/components/{tryghost-mailgun-client-5.115.0.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
  27. package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
  28. package/components/{tryghost-member-events-5.115.0.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
  29. package/components/{tryghost-members-api-5.115.0.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
  30. package/components/{tryghost-members-csv-5.115.0.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
  31. package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
  32. package/components/{tryghost-members-payments-5.115.0.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
  33. package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
  34. package/components/{tryghost-minifier-5.115.0.tgz → tryghost-minifier-5.115.1.tgz} +0 -0
  35. package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
  36. package/components/{tryghost-mw-version-match-5.115.0.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
  37. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  38. package/components/tryghost-post-events-5.115.1.tgz +0 -0
  39. package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
  40. package/components/{tryghost-posts-service-5.115.0.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
  41. package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
  42. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  43. package/components/{tryghost-security-5.115.0.tgz → tryghost-security-5.115.1.tgz} +0 -0
  44. package/components/{tryghost-slack-notifications-5.115.0.tgz → tryghost-slack-notifications-5.115.1.tgz} +0 -0
  45. package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
  46. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  47. package/content/themes/casper/LICENSE +1 -1
  48. package/content/themes/casper/README.md +1 -1
  49. package/content/themes/source/LICENSE +1 -1
  50. package/content/themes/source/README.md +1 -1
  51. package/content/themes/source/assets/built/screen.css +1 -1
  52. package/content/themes/source/assets/built/screen.css.map +1 -1
  53. package/content/themes/source/assets/css/screen.css +11 -6
  54. package/content/themes/source/partials/feature-image.hbs +2 -2
  55. package/core/boot.js +3 -1
  56. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
  57. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  58. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
  59. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
  60. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
  61. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  62. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
  63. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
  64. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
  65. package/core/built/admin/assets/{chunk.524.31419fdf6fb3859ecc1e.js → chunk.524.2439684964c164c598ab.js} +6 -6
  66. package/core/built/admin/assets/{chunk.582.08c816d5e4ab766486a7.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
  67. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
  68. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
  69. package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +181 -159
  70. package/core/built/admin/assets/stats/stats.js +11824 -0
  71. package/core/built/admin/index.html +4 -4
  72. package/core/frontend/helpers/ghost_head.js +3 -1
  73. package/core/frontend/src/cards/css/cta.css +1 -1
  74. package/core/server/api/endpoints/slugs.js +6 -2
  75. package/core/server/data/importer/import-manager.js +2 -2
  76. package/core/server/data/importer/importers/importer-revue.js +128 -0
  77. package/core/server/data/importer/importers/json-to-html.js +107 -0
  78. package/core/server/data/migrations/utils/tables.js +2 -4
  79. package/core/server/lib/bootstrap-socket.js +87 -0
  80. package/core/server/lib/package-json/index.js +1 -0
  81. package/core/server/lib/package-json/package-json.js +160 -0
  82. package/core/server/lib/package-json/parse.js +57 -0
  83. package/core/server/models/base/plugins/actions.js +44 -31
  84. package/core/server/models/base/plugins/generate-slug.js +6 -0
  85. package/core/server/notify.js +1 -1
  86. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  87. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  88. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  89. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  90. package/core/server/services/api-version-compatibility/index.js +2 -2
  91. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  92. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  93. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  94. package/core/server/services/audience-feedback/Feedback.js +35 -0
  95. package/core/server/services/audience-feedback/index.js +4 -2
  96. package/core/server/services/auth/session/emails/signin.js +168 -0
  97. package/core/server/services/auth/session/index.js +2 -2
  98. package/core/server/services/auth/session/session-from-token.js +69 -0
  99. package/core/server/services/auth/session/session-service.js +364 -0
  100. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  101. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  102. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  103. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  104. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  105. package/core/server/services/explore-ping/index.js +31 -0
  106. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  107. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  108. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  109. package/core/server/services/invitations/accept.js +5 -2
  110. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  111. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  112. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  113. package/core/server/services/mail-events/MailEvent.js +20 -0
  114. package/core/server/services/mail-events/MailEvent.ts +10 -0
  115. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  116. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  117. package/core/server/services/mail-events/MailEventService.js +124 -0
  118. package/core/server/services/mail-events/MailEventService.ts +169 -0
  119. package/core/server/services/mail-events/index.js +1 -1
  120. package/core/server/services/mail-events/libraries.d.ts +2 -0
  121. package/core/server/services/members/CaptchaService.js +80 -0
  122. package/core/server/services/members/api.js +1 -1
  123. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  124. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  125. package/core/server/services/members/importer/email-template.js +182 -0
  126. package/core/server/services/members/importer/index.js +30 -0
  127. package/core/server/services/members/members-ssr.js +333 -0
  128. package/core/server/services/members/service.js +2 -2
  129. package/core/server/services/posts/stats/PostStats.js +13 -0
  130. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  131. package/core/server/services/route-settings/index.js +1 -1
  132. package/core/server/services/stripe/README.md +63 -0
  133. package/core/server/services/stripe/StripeAPI.js +931 -0
  134. package/core/server/services/stripe/StripeMigrations.js +613 -0
  135. package/core/server/services/stripe/StripeService.js +175 -0
  136. package/core/server/services/stripe/WebhookController.js +100 -0
  137. package/core/server/services/stripe/WebhookManager.js +175 -0
  138. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  139. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  140. package/core/server/services/stripe/events/index.js +4 -0
  141. package/core/server/services/stripe/service.js +1 -1
  142. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  143. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  144. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  145. package/core/server/services/themes/loader.js +1 -1
  146. package/core/server/services/themes/to-json.js +1 -1
  147. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  148. package/core/server/web/shared/middleware/cache-control.js +51 -0
  149. package/core/server/web/shared/middleware/index.js +1 -1
  150. package/core/server/web/well-known.js +1 -1
  151. package/core/shared/labs.js +3 -1
  152. package/core/shared/settings-cache/CacheManager.js +64 -6
  153. package/package.json +103 -134
  154. package/tsconfig.tsbuildinfo +1 -1
  155. package/yarn.lock +7 -93
  156. package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
  157. package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
  158. package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
  159. package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
  160. package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
  161. package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
  162. package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
  163. package/components/tryghost-constants-5.115.0.tgz +0 -0
  164. package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
  165. package/components/tryghost-domain-events-5.115.0.tgz +0 -0
  166. package/components/tryghost-donations-5.115.0.tgz +0 -0
  167. package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
  168. package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
  169. package/components/tryghost-email-service-5.115.0.tgz +0 -0
  170. package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
  171. package/components/tryghost-ghost-5.115.0.tgz +0 -0
  172. package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
  173. package/components/tryghost-i18n-5.115.0.tgz +0 -0
  174. package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
  175. package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
  176. package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
  177. package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
  178. package/components/tryghost-mail-events-5.115.0.tgz +0 -0
  179. package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
  180. package/components/tryghost-members-importer-5.115.0.tgz +0 -0
  181. package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
  182. package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
  183. package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
  184. package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
  185. package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
  186. package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
  187. package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
  188. package/components/tryghost-package-json-5.115.0.tgz +0 -0
  189. package/components/tryghost-post-events-5.115.0.tgz +0 -0
  190. package/components/tryghost-recommendations-5.115.0.tgz +0 -0
  191. package/components/tryghost-referrers-5.115.0.tgz +0 -0
  192. package/components/tryghost-session-service-5.115.0.tgz +0 -0
  193. package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
  194. package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
  195. package/components/tryghost-webmentions-5.115.0.tgz +0 -0
  196. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  197. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
@@ -3,9 +3,20 @@ const errors = require('@tryghost/errors');
3
3
  const logging = require('@tryghost/logging');
4
4
 
5
5
  /**
6
+ * This plugin is used to add actions to the database. It backs the "audit log" feature we have in Ghost.
7
+ *
8
+ * The functions here are triggered by the `onCreated`, `onUpdated`, `onDeleted` functions in the `events`
9
+ * plugin, with some extra ones for niche other events.
10
+ *
6
11
  * @param {import('bookshelf')} Bookshelf
7
12
  */
8
13
  module.exports = function (Bookshelf) {
14
+ /**
15
+ * Insert an action into the database
16
+ *
17
+ * @param {Object} data - The data to insert
18
+ * @param {Object} options - The options object
19
+ */
9
20
  const insertAction = (data, options) => {
10
21
  // CASE: model does not support action for target event
11
22
  if (!data) {
@@ -39,7 +50,13 @@ module.exports = function (Bookshelf) {
39
50
  }
40
51
  };
41
52
 
42
- // We need this addAction accessible from the static model and instances
53
+ /**
54
+ * Add an action to the database
55
+ *
56
+ * @param {import('bookshelf').Model} model - The model to add the action to
57
+ * @param {string} event - The event that triggered the action
58
+ * @param {Object} options - The options object
59
+ */
43
60
  const addAction = (model, event, options) => {
44
61
  if (!model.wasChanged()) {
45
62
  return;
@@ -58,11 +75,14 @@ module.exports = function (Bookshelf) {
58
75
  /**
59
76
  * Constructs data to be stored in the database with info
60
77
  * on particular actions
78
+ *
79
+ * @param {string} event - The event that triggered the action
80
+ * @param {Object} options - The options object
81
+ * @returns {Object} The data to be stored in the database
61
82
  */
62
83
  getAction(event, options) {
84
+ // Ignore internal updates (`options.context.internal`) for now
63
85
  const actor = this.getActor(options);
64
-
65
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
66
86
  if (!actor) {
67
87
  return;
68
88
  }
@@ -71,12 +91,7 @@ module.exports = function (Bookshelf) {
71
91
  return;
72
92
  }
73
93
 
74
- let resourceType = this.actionsResourceType;
75
-
76
- if (typeof resourceType === 'function') {
77
- resourceType = resourceType.bind(this)();
78
- }
79
-
94
+ const resourceType = this.actionsResourceType;
80
95
  if (!resourceType) {
81
96
  return;
82
97
  }
@@ -85,12 +100,14 @@ module.exports = function (Bookshelf) {
85
100
  action_name: options.actionName
86
101
  };
87
102
 
103
+ // Used to attach extra content to the action (ie. the key + group for settings changes)
88
104
  if (this.actionsExtraContext && Array.isArray(this.actionsExtraContext)) {
89
105
  for (const c of this.actionsExtraContext) {
90
106
  context[c] = this.get(c) || this.previous(c);
91
107
  }
92
108
  }
93
109
 
110
+ // Attach the primary name to the action (ie. the title or name of the model)
94
111
  if (event === 'deleted') {
95
112
  context.primary_name = (this.previous('title') || this.previous('name'));
96
113
  } else if (['added', 'edited'].includes(event)) {
@@ -112,21 +129,17 @@ module.exports = function (Bookshelf) {
112
129
  return data;
113
130
  },
114
131
 
115
- /**
116
- * @NOTE:
117
- *
118
- * We add actions step by step and define how they should look like.
119
- * Each post update triggers a couple of events, which we don't want to add actions for.
120
- *
121
- * e.g. transform post to page triggers a handful of events including `post.deleted` and `page.added`
122
- *
123
- * We protect adding too many and uncontrolled events.
124
- *
125
- * We could embed adding actions more nicely in the future e.g. plugin.
126
- */
127
132
  addAction
128
133
  }, {
129
134
  addAction,
135
+
136
+ /**
137
+ * Add actions for bulk actions
138
+ *
139
+ * @param {string} event - The event that triggered the action
140
+ * @param {number[]} ids - The ids of the models that were affected
141
+ * @param {Object} options - The options object
142
+ */
130
143
  async addActions(event, ids, options) {
131
144
  if (ids.length === 1) {
132
145
  // We want to store an event for a single model in the actions table
@@ -141,27 +154,27 @@ module.exports = function (Bookshelf) {
141
154
  },
142
155
 
143
156
  /**
144
- * Constructs data to be stored in the database with info
145
- * on particular actions
157
+ * Constructs data for bulk actions to be stored in the database
158
+ *
159
+ * @param {string} event - The event that triggered the action
160
+ * @param {number} count - The number of models that were affected
161
+ * @param {Object} options - The options object
162
+ * @returns {Object} The data to be stored in the database
146
163
  */
147
164
  getBulkAction(event, count, options) {
165
+ // Ignore internal updates (`options.context.internal`) for now
148
166
  const actor = this.prototype.getActor(options);
149
-
150
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
151
167
  if (!actor) {
152
168
  return;
153
169
  }
154
170
 
171
+ // Models can opt-in to their CRUD actions being collected (we do this so we don't
172
+ // log every single action)
155
173
  if (!this.prototype.actionsCollectCRUD) {
156
174
  return;
157
175
  }
158
176
 
159
- let resourceType = this.prototype.actionsResourceType;
160
-
161
- if (typeof resourceType === 'function') {
162
- resourceType = resourceType.bind(this)();
163
- }
164
-
177
+ const resourceType = this.prototype.actionsResourceType;
165
178
  if (!resourceType) {
166
179
  return;
167
180
  }
@@ -39,6 +39,12 @@ module.exports = function (Bookshelf) {
39
39
  return slugToFind;
40
40
  }
41
41
 
42
+ if (options.modelId) {
43
+ if (found.id === options.modelId) {
44
+ return slugToFind;
45
+ }
46
+ }
47
+
42
48
  slugTryCount += 1;
43
49
 
44
50
  // If we shortened, go back to the full version and try again
@@ -51,7 +51,7 @@ async function notify(type, error = null) {
51
51
  // CASE: use bootstrap socket to communicate with CLI for systemd
52
52
  let socketAddress = config.get('bootstrap-socket');
53
53
  if (socketAddress) {
54
- const bootstrapSocket = require('@tryghost/bootstrap-socket');
54
+ const bootstrapSocket = require('./lib/bootstrap-socket');
55
55
  return bootstrapSocket.connectAndSend(socketAddress, message);
56
56
  }
57
57
 
@@ -1,6 +1,6 @@
1
1
  import ObjectID from 'bson-objectid';
2
2
  import {Knex} from 'knex';
3
- import {IdentityTokenService} from '@tryghost/identity-token-service';
3
+ import {IdentityTokenService} from '../identity-tokens/IdentityTokenService';
4
4
  import fetch from 'node-fetch';
5
5
 
6
6
  type ExpectedWebhook = {
@@ -0,0 +1,99 @@
1
+ const path = require('path');
2
+ const VersionNotificationsDataService = require('./VersionNotificationsDataService');
3
+ const EmailContentGenerator = require('@tryghost/email-content-generator');
4
+
5
+ class APIVersionCompatibilityService {
6
+ /**
7
+ *
8
+ * @param {Object} options
9
+ * @param {Object} options.UserModel - ghost user model
10
+ * @param {Object} options.ApiKeyModel - ghost api key model
11
+ * @param {Object} options.settingsService - ghost settings service
12
+ * @param {(Object: {subject: String, to: String, text: String, html: String}) => Promise<any>} options.sendEmail - email sending function
13
+ * @param {Function} options.getSiteUrl
14
+ * @param {Function} options.getSiteTitle
15
+ */
16
+ constructor({UserModel, ApiKeyModel, settingsService, sendEmail, getSiteUrl, getSiteTitle}) {
17
+ this.sendEmail = sendEmail;
18
+
19
+ this.versionNotificationsDataService = new VersionNotificationsDataService({
20
+ UserModel,
21
+ ApiKeyModel,
22
+ settingsService
23
+ });
24
+
25
+ this.emailContentGenerator = new EmailContentGenerator({
26
+ getSiteUrl,
27
+ getSiteTitle,
28
+ templatesDir: path.join(__dirname, 'templates')
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Version mismatch handler doing the logic of picking a template and sending a notification email
34
+ * @param {Object} options
35
+ * @param {string} options.acceptVersion - client's accept-version header value
36
+ * @param {string} options.contentVersion - server's content-version header value
37
+ * @param {string} options.apiKeyValue - key value (secret for Content API and kid for Admin API) used to access the API
38
+ * @param {string} options.apiKeyType - key type used to access the API
39
+ * @param {string} options.requestURL - url that was requested and failed compatibility test
40
+ * @param {string} [options.userAgent] - client's user-agent header value
41
+ */
42
+ async handleMismatch({acceptVersion, contentVersion, apiKeyValue, apiKeyType, requestURL, userAgent = ''}) {
43
+ if (!await this.versionNotificationsDataService.fetchNotification(acceptVersion)) {
44
+ const integration = await this.versionNotificationsDataService.getIntegration(apiKeyValue, apiKeyType);
45
+
46
+ // We couldn't find the integration
47
+ if (!integration) {
48
+ return;
49
+ }
50
+
51
+ const {
52
+ name: integrationName,
53
+ type: integrationType
54
+ } = integration;
55
+
56
+ // @NOTE: "internal" or "core" integrations (https://ghost.notion.site/Data-Types-e5dc54dd0078443f9afd6b2abda443c4)
57
+ // are maintained by Ghost team, so there is no sense notifying the instance owner about it's incompatibility.
58
+ // The other two integration types: "builtin" and "custom", is when we want to notify about incompatibility.
59
+ if (['internal', 'core'].includes(integrationType)) {
60
+ return;
61
+ }
62
+
63
+ const trimmedUseAgent = userAgent.split('/')[0];
64
+ const emails = await this.versionNotificationsDataService.getNotificationEmails();
65
+
66
+ for (const email of emails) {
67
+ const template = (trimmedUseAgent === 'Zapier')
68
+ ? 'zapier-mismatch'
69
+ : 'generic-mismatch';
70
+
71
+ const subject = (trimmedUseAgent === 'Zapier')
72
+ ? 'Attention required: One of your Zaps has failed'
73
+ : `Attention required: Your ${integrationName} integration has failed`;
74
+
75
+ const {html, text} = await this.emailContentGenerator.getContent({
76
+ template,
77
+ data: {
78
+ acceptVersion,
79
+ contentVersion,
80
+ clientName: integrationName,
81
+ recipientEmail: email,
82
+ requestURL: requestURL
83
+ }
84
+ });
85
+
86
+ await this.sendEmail({
87
+ subject,
88
+ to: email,
89
+ html,
90
+ text
91
+ });
92
+ }
93
+
94
+ await this.versionNotificationsDataService.saveNotification(acceptVersion);
95
+ }
96
+ }
97
+ }
98
+
99
+ module.exports = APIVersionCompatibilityService;
@@ -0,0 +1,80 @@
1
+ const internalContext = {
2
+ internal: true
3
+ };
4
+
5
+ class VersionNotificationsDataService {
6
+ /**
7
+ * @param {Object} options
8
+ * @param {Object} options.UserModel - ghost user model
9
+ * @param {Object} options.ApiKeyModel - ghost api key model
10
+ * @param {Object} options.settingsService - ghost settings service
11
+ */
12
+ constructor({UserModel, ApiKeyModel, settingsService}) {
13
+ this.UserModel = UserModel;
14
+ this.ApiKeyModel = ApiKeyModel;
15
+ this.settingsService = settingsService;
16
+ }
17
+
18
+ async fetchNotification(acceptVersion) {
19
+ const setting = await this.settingsService.read('version_notifications', internalContext);
20
+ const versionNotifications = JSON.parse(setting.version_notifications.value);
21
+
22
+ return versionNotifications.find(version => version === acceptVersion);
23
+ }
24
+
25
+ async saveNotification(acceptVersion) {
26
+ const setting = await this.settingsService.read('version_notifications', internalContext);
27
+ const versionNotifications = JSON.parse(setting.version_notifications.value);
28
+
29
+ if (!versionNotifications.find(version => version === acceptVersion)) {
30
+ versionNotifications.push(acceptVersion);
31
+
32
+ return this.settingsService.edit([{
33
+ key: 'version_notifications',
34
+ value: JSON.stringify(versionNotifications)
35
+ }], {
36
+ context: internalContext
37
+ });
38
+ }
39
+ }
40
+
41
+ async getNotificationEmails() {
42
+ const data = await this.UserModel.findAll(Object.assign({
43
+ withRelated: ['roles'],
44
+ filter: 'status:active'
45
+ }, internalContext));
46
+
47
+ const adminEmails = data
48
+ .toJSON()
49
+ .filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
50
+ .map(user => user.email);
51
+
52
+ return adminEmails;
53
+ }
54
+
55
+ /**
56
+ * This method is for internal use only.
57
+ *
58
+ * @param {String} key - api key identification value, it's "secret" in case of Content API key and "id" for Admin API
59
+ * @param {String} type - one of "content" or "admin" values
60
+ * @returns {Promise<Object | null>} Integration JSON object
61
+ */
62
+ async getIntegration(key, type) {
63
+ let queryOptions = null;
64
+
65
+ if (type === 'content') {
66
+ queryOptions = {secret: key};
67
+ } else if (type === 'admin') {
68
+ queryOptions = {id: key};
69
+ }
70
+
71
+ const apiKey = await this.ApiKeyModel.findOne(queryOptions, {withRelated: ['integration']});
72
+ if (!apiKey) {
73
+ return null;
74
+ }
75
+
76
+ return apiKey.relations.integration.toJSON();
77
+ }
78
+ }
79
+
80
+ module.exports = VersionNotificationsDataService;
@@ -0,0 +1,57 @@
1
+ const jwt = require('jsonwebtoken');
2
+
3
+ /**
4
+ * Remove 'Ghost' from raw authorization header and extract the JWT token.
5
+ * Eg. Authorization: Ghost ${JWT}
6
+ * @param {string} header
7
+ */
8
+ const extractTokenFromHeader = (header) => {
9
+ const [scheme, token] = header.split(' ');
10
+
11
+ if (/^Ghost$/i.test(scheme)) {
12
+ return token;
13
+ }
14
+ };
15
+
16
+ const extractAdminAPIKey = (token) => {
17
+ const decoded = jwt.decode(token, {complete: true});
18
+
19
+ if (!decoded || !decoded.header || !decoded.header.kid) {
20
+ return null;
21
+ }
22
+
23
+ return decoded.header.kid;
24
+ };
25
+
26
+ /**
27
+ * @typedef {object} ApiKey
28
+ * @prop {string} key
29
+ * @prop {string} type
30
+ */
31
+
32
+ /**
33
+ * When it's a Content API the function resolves with the value of the key secret.
34
+ * When it's an Admin API the function resolves with the value of the key id.
35
+ *
36
+ * @param {import('express').Request} req
37
+ * @returns {ApiKey}
38
+ */
39
+ const extractAPIKey = (req) => {
40
+ let keyValue = null;
41
+ let keyType = null;
42
+
43
+ if (req.query && req.query.key) {
44
+ keyValue = req.query.key;
45
+ keyType = 'content';
46
+ } else if (req.headers && req.headers.authorization) {
47
+ keyValue = extractAdminAPIKey(extractTokenFromHeader(req.headers.authorization));
48
+ keyType = 'admin';
49
+ }
50
+
51
+ return {
52
+ key: keyValue,
53
+ type: keyType
54
+ };
55
+ };
56
+
57
+ module.exports = extractAPIKey;
@@ -1,5 +1,5 @@
1
- const APIVersionCompatibilityService = require('@tryghost/api-version-compatibility-service');
2
- const versionMismatchHandler = require('@tryghost/mw-api-version-mismatch');
1
+ const APIVersionCompatibilityService = require('./APIVersionCompatibilityService');
2
+ const versionMismatchHandler = require('./mw-api-version-mismatch');
3
3
  const ghostVersion = require('@tryghost/version');
4
4
  const {GhostMailer} = require('../mail');
5
5
  const settingsService = require('../settings/settings-service');
@@ -0,0 +1,31 @@
1
+ const extractApiKey = require('./extract-api-key');
2
+
3
+ const versionMismatchHandler = (APIVersionCompatibilityService) => {
4
+ /**
5
+ * @param {Object} err
6
+ * @param {import('express').Request} req
7
+ * @param {import('express').Response} res
8
+ * @param {import('express').NextFunction} next
9
+ */
10
+ return async function versionMismatchHandlerMiddleware(err, req, res, next) {
11
+ if (err && err.errorType === 'RequestNotAcceptableError') {
12
+ if (err.code === 'UPDATE_CLIENT') {
13
+ const {key, type} = extractApiKey(req);
14
+ const requestURL = req.originalUrl.split('?').shift();
15
+
16
+ await APIVersionCompatibilityService.handleMismatch({
17
+ acceptVersion: req.headers['accept-version'],
18
+ contentVersion: `v${res.locals.safeVersion}`,
19
+ requestURL,
20
+ userAgent: req.headers['user-agent'],
21
+ apiKeyValue: key,
22
+ apiKeyType: type
23
+ });
24
+ }
25
+ }
26
+
27
+ next(err);
28
+ };
29
+ };
30
+
31
+ module.exports = versionMismatchHandler;
@@ -0,0 +1,85 @@
1
+ const Feedback = require('./Feedback');
2
+ const errors = require('@tryghost/errors');
3
+ const tpl = require('@tryghost/tpl');
4
+
5
+ const messages = {
6
+ invalidScore: 'Invalid feedback score. Only 1 or 0 is currently allowed.',
7
+ postNotFound: 'Post not found.',
8
+ memberNotFound: 'Member not found.'
9
+ };
10
+
11
+ /**
12
+ * @typedef {object} IFeedbackRepository
13
+ * @prop {(feedback: Feedback) => Promise<void>} add
14
+ * @prop {(feedback: Feedback) => Promise<void>} edit
15
+ * @prop {(postId, memberId) => Promise<Feedback>} get
16
+ * @prop {(id: string) => Promise<Post|undefined>} getPostById
17
+ */
18
+
19
+ class AudienceFeedbackController {
20
+ /** @type IFeedbackRepository */
21
+ #repository;
22
+
23
+ /**
24
+ * @param {object} deps
25
+ * @param {IFeedbackRepository} deps.repository
26
+ */
27
+ constructor(deps) {
28
+ this.#repository = deps.repository;
29
+ }
30
+
31
+ /**
32
+ * Get member from frame
33
+ */
34
+ #getMember(frame) {
35
+ if (!frame.options?.context?.member?.id) {
36
+ // This is an internal server error because authentication should happen outside this service.
37
+ throw new errors.InternalServerError({
38
+ message: tpl(messages.memberNotFound)
39
+ });
40
+ }
41
+ return frame.options.context.member;
42
+ }
43
+
44
+ async add(frame) {
45
+ const data = frame.data.feedback[0];
46
+ const postId = data.post_id;
47
+ const score = data.score;
48
+
49
+ if (![0, 1].includes(score)) {
50
+ throw new errors.ValidationError({
51
+ message: tpl(messages.invalidScore)
52
+ });
53
+ }
54
+
55
+ const member = this.#getMember(frame);
56
+
57
+ const post = await this.#repository.getPostById(postId);
58
+ if (!post) {
59
+ throw new errors.NotFoundError({
60
+ message: tpl(messages.postNotFound)
61
+ });
62
+ }
63
+
64
+ const existing = await this.#repository.get(post.id, member.id);
65
+ if (existing) {
66
+ if (existing.score === score) {
67
+ // Don't save so we don't update the updated_at timestamp
68
+ return existing;
69
+ }
70
+ existing.score = score;
71
+ await this.#repository.edit(existing);
72
+ return existing;
73
+ }
74
+
75
+ const feedback = new Feedback({
76
+ memberId: member.id,
77
+ postId: post.id,
78
+ score
79
+ });
80
+ await this.#repository.add(feedback);
81
+ return feedback;
82
+ }
83
+ }
84
+
85
+ module.exports = AudienceFeedbackController;
@@ -0,0 +1,34 @@
1
+ class AudienceFeedbackService {
2
+ /** @type URL */
3
+ #baseURL;
4
+ /** @type {Object} */
5
+ #urlService;
6
+ /**
7
+ * @param {object} deps
8
+ * @param {object} deps.config
9
+ * @param {URL} deps.config.baseURL
10
+ * @param {object} deps.urlService
11
+ */
12
+ constructor(deps) {
13
+ this.#baseURL = deps.config.baseURL;
14
+ this.#urlService = deps.urlService;
15
+ }
16
+ /**
17
+ * @param {string} uuid
18
+ * @param {string} postId
19
+ * @param {0 | 1} score
20
+ * @param {string} key - hashed uuid value
21
+ */
22
+ buildLink(uuid, postId, score, key) {
23
+ let postUrl = this.#urlService.getUrlByResourceId(postId, {absolute: true});
24
+
25
+ if (postUrl.match(/\/404\//)) {
26
+ postUrl = this.#baseURL;
27
+ }
28
+ const url = new URL(postUrl);
29
+ url.hash = `#/feedback/${postId}/${score}/?uuid=${encodeURIComponent(uuid)}&key=${encodeURIComponent(key)}`;
30
+ return url;
31
+ }
32
+ }
33
+
34
+ module.exports = AudienceFeedbackService;
@@ -0,0 +1,35 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+
3
+ module.exports = class Feedback {
4
+ /** @type {ObjectID} */
5
+ id;
6
+ /** @type {number} */
7
+ score;
8
+ /** @type {ObjectID} */
9
+ memberId;
10
+ /** @type {ObjectID} */
11
+ postId;
12
+
13
+ constructor(data) {
14
+ if (!data.id) {
15
+ this.id = new ObjectID();
16
+ }
17
+
18
+ if (typeof data.id === 'string') {
19
+ this.id = ObjectID.createFromHexString(data.id);
20
+ }
21
+
22
+ this.score = data.score ?? 0;
23
+ if (typeof data.memberId === 'string') {
24
+ this.memberId = ObjectID.createFromHexString(data.memberId);
25
+ } else {
26
+ this.memberId = data.memberId;
27
+ }
28
+
29
+ if (typeof data.postId === 'string') {
30
+ this.postId = ObjectID.createFromHexString(data.postId);
31
+ } else {
32
+ this.postId = data.postId;
33
+ }
34
+ }
35
+ };
@@ -1,5 +1,9 @@
1
1
  const urlUtils = require('../../../shared/url-utils');
2
2
  const urlService = require('../../services/url');
3
+
4
+ const AudienceFeedbackService = require('./AudienceFeedbackService');
5
+ const AudienceFeedbackController = require('./AudienceFeedbackController');
6
+ const Feedback = require('./Feedback');
3
7
  const FeedbackRepository = require('./FeedbackRepository');
4
8
 
5
9
  class AudienceFeedbackServiceWrapper {
@@ -12,8 +16,6 @@ class AudienceFeedbackServiceWrapper {
12
16
  // Wire up all the dependencies
13
17
  const models = require('../../models');
14
18
 
15
- const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
16
-
17
19
  this.repository = new FeedbackRepository({
18
20
  Member: models.Member,
19
21
  MemberFeedback: models.MemberFeedback,