ghost 5.10.1 → 5.12.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 (150) hide show
  1. package/components/{tryghost-adapter-manager-5.10.1.tgz → tryghost-adapter-manager-5.12.1.tgz} +0 -0
  2. package/components/tryghost-api-framework-5.12.1.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-5.12.1.tgz +0 -0
  4. package/components/{tryghost-bootstrap-socket-5.10.1.tgz → tryghost-bootstrap-socket-5.12.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.12.1.tgz +0 -0
  6. package/components/{tryghost-custom-theme-settings-service-5.10.1.tgz → tryghost-custom-theme-settings-service-5.12.1.tgz} +0 -0
  7. package/components/tryghost-domain-events-5.12.1.tgz +0 -0
  8. package/components/{tryghost-email-analytics-provider-mailgun-5.10.1.tgz → tryghost-email-analytics-provider-mailgun-5.12.1.tgz} +0 -0
  9. package/components/{tryghost-email-analytics-service-5.10.1.tgz → tryghost-email-analytics-service-5.12.1.tgz} +0 -0
  10. package/components/{tryghost-email-content-generator-5.10.1.tgz → tryghost-email-content-generator-5.12.1.tgz} +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.12.1.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.12.1.tgz +0 -0
  13. package/components/{tryghost-html-to-plaintext-5.10.1.tgz → tryghost-html-to-plaintext-5.12.1.tgz} +0 -0
  14. package/components/{tryghost-job-manager-5.10.1.tgz → tryghost-job-manager-5.12.1.tgz} +0 -0
  15. package/components/tryghost-magic-link-5.12.1.tgz +0 -0
  16. package/components/tryghost-mailgun-client-5.12.1.tgz +0 -0
  17. package/components/tryghost-member-analytics-service-5.12.1.tgz +0 -0
  18. package/components/tryghost-member-attribution-5.12.1.tgz +0 -0
  19. package/components/tryghost-member-events-5.12.1.tgz +0 -0
  20. package/components/{tryghost-members-analytics-ingress-5.10.1.tgz → tryghost-members-analytics-ingress-5.12.1.tgz} +0 -0
  21. package/components/tryghost-members-api-5.12.1.tgz +0 -0
  22. package/components/{tryghost-members-csv-5.10.1.tgz → tryghost-members-csv-5.12.1.tgz} +0 -0
  23. package/components/{tryghost-members-events-service-5.10.1.tgz → tryghost-members-events-service-5.12.1.tgz} +0 -0
  24. package/components/tryghost-members-importer-5.12.1.tgz +0 -0
  25. package/components/tryghost-members-offers-5.12.1.tgz +0 -0
  26. package/components/{tryghost-members-payments-5.10.1.tgz → tryghost-members-payments-5.12.1.tgz} +0 -0
  27. package/components/tryghost-members-ssr-5.12.1.tgz +0 -0
  28. package/components/{tryghost-members-stripe-service-5.10.1.tgz → tryghost-members-stripe-service-5.12.1.tgz} +0 -0
  29. package/components/tryghost-minifier-5.12.1.tgz +0 -0
  30. package/components/tryghost-mw-api-version-mismatch-5.12.1.tgz +0 -0
  31. package/components/{tryghost-mw-cache-control-5.10.1.tgz → tryghost-mw-cache-control-5.12.1.tgz} +0 -0
  32. package/components/tryghost-mw-error-handler-5.12.1.tgz +0 -0
  33. package/components/{tryghost-mw-session-from-token-5.10.1.tgz → tryghost-mw-session-from-token-5.12.1.tgz} +0 -0
  34. package/components/tryghost-mw-update-user-last-seen-5.12.1.tgz +0 -0
  35. package/components/tryghost-mw-vhost-5.12.1.tgz +0 -0
  36. package/components/{tryghost-oembed-service-5.10.1.tgz → tryghost-oembed-service-5.12.1.tgz} +0 -0
  37. package/components/{tryghost-package-json-5.10.1.tgz → tryghost-package-json-5.12.1.tgz} +0 -0
  38. package/components/{tryghost-security-5.10.1.tgz → tryghost-security-5.12.1.tgz} +0 -0
  39. package/components/{tryghost-session-service-5.10.1.tgz → tryghost-session-service-5.12.1.tgz} +0 -0
  40. package/components/tryghost-settings-path-manager-5.12.1.tgz +0 -0
  41. package/components/tryghost-staff-service-5.12.1.tgz +0 -0
  42. package/components/{tryghost-update-check-service-5.10.1.tgz → tryghost-update-check-service-5.12.1.tgz} +0 -0
  43. package/components/tryghost-verification-trigger-5.12.1.tgz +0 -0
  44. package/components/{tryghost-version-notifications-data-service-5.10.1.tgz → tryghost-version-notifications-data-service-5.12.1.tgz} +0 -0
  45. package/core/boot.js +2 -0
  46. package/core/built/admin/assets/chunk.143.cae75c9fa9b7050ffee8.js +49 -0
  47. package/core/built/admin/assets/{chunk.174.0364e8abdae8210d8e6d.js → chunk.174.ae492405065373dbe102.js} +1 -1
  48. package/core/built/admin/assets/{chunk.178.bb46965ba0483c3e79ae.js → chunk.178.0487ee9895eefb6e2baf.js} +4 -4
  49. package/core/built/admin/assets/{chunk.351.ea4a4ff4b40d5f2ad141.js → chunk.579.65e09dd89eec70d059a0.js} +3 -11
  50. package/core/built/admin/assets/{chunk.351.ea4a4ff4b40d5f2ad141.js.LICENSE.txt → chunk.579.65e09dd89eec70d059a0.js.LICENSE.txt} +0 -0
  51. package/core/built/admin/assets/{ghost-ced03a7ac75c3148e0ea7d1bf51e39fc.js → ghost-0526c96b20843697927c1d06a9010197.js} +779 -610
  52. package/core/built/admin/assets/ghost-0bbbef127e5dc0c0651fc442c4fdba8e.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-27e002e66fbfdfaf3efcde63d5429c38.css +1 -0
  54. package/core/built/admin/assets/icons/calendar-stroke.svg +1 -0
  55. package/core/built/admin/assets/icons/event-canceled-subscription--feature-attribution.svg +6 -0
  56. package/core/built/admin/assets/icons/event-comment--feature-attribution.svg +3 -0
  57. package/core/built/admin/assets/icons/event-email-delivery-failed--feature-attribution.svg +6 -0
  58. package/core/built/admin/assets/icons/event-logged-in--feature-attribution.svg +5 -0
  59. package/core/built/admin/assets/icons/event-made-a-payment--feature-attribution.svg +7 -0
  60. package/core/built/admin/assets/icons/event-opened-email--feature-attribution.svg +6 -0
  61. package/core/built/admin/assets/icons/event-received-email--feature-attribution.svg +5 -0
  62. package/core/built/admin/assets/icons/event-signed-up--feature-attribution.svg +6 -0
  63. package/core/built/admin/assets/icons/event-started-subscription--feature-attribution.svg +6 -0
  64. package/core/built/admin/assets/icons/event-subscribed-to-email--feature-attribution.svg +8 -0
  65. package/core/built/admin/assets/icons/event-subscriptions--feature-attribution.svg +5 -0
  66. package/core/built/admin/assets/icons/event-unsubscribed-from-email--feature-attribution.svg +5 -0
  67. package/core/built/admin/assets/icons/pen-stroke.svg +1 -0
  68. package/core/built/admin/assets/{vendor-a1ae7a38d5c38fcba5609eed4e37f02a.js → vendor-52613f40d62355e9ac64cbfa211169bb.js} +88 -60
  69. package/core/built/admin/index.html +6 -6
  70. package/core/frontend/helpers/search.js +5 -20
  71. package/core/frontend/meta/get-meta.js +1 -2
  72. package/core/frontend/meta/image-dimensions.js +47 -39
  73. package/core/server/api/endpoints/comments-members.js +10 -7
  74. package/core/server/api/endpoints/invites.js +1 -9
  75. package/core/server/api/endpoints/labels.js +1 -7
  76. package/core/server/api/endpoints/members.js +3 -13
  77. package/core/server/api/endpoints/offers.js +2 -2
  78. package/core/server/api/endpoints/pages.js +2 -10
  79. package/core/server/api/endpoints/posts.js +1 -9
  80. package/core/server/api/endpoints/settings.js +0 -94
  81. package/core/server/api/endpoints/snippets.js +1 -9
  82. package/core/server/api/endpoints/tags.js +1 -7
  83. package/core/server/api/endpoints/utils/serializers/input/pages.js +1 -1
  84. package/core/server/api/endpoints/utils/serializers/output/members.js +2 -1
  85. package/core/server/api/endpoints/utils/serializers/output/site.js +1 -0
  86. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +6 -7
  87. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -20
  88. package/core/server/api/endpoints/webhooks.js +2 -19
  89. package/core/server/data/migrations/versions/5.11/2022-08-22-11-03-add-member-alert-settings-columns-to-users.js +21 -0
  90. package/core/server/data/migrations/versions/5.11/2022-08-23-13-41-backfill-members-created-events.js +32 -0
  91. package/core/server/data/migrations/versions/5.11/2022-08-23-13-59-fix-page-resource-type.js +22 -0
  92. package/core/server/data/schema/fixtures/fixtures.json +3 -0
  93. package/core/server/data/schema/schema.js +23 -4
  94. package/core/server/lib/image/gravatar.js +8 -7
  95. package/core/server/lib/image/image-size.js +60 -56
  96. package/core/server/models/action.js +6 -19
  97. package/core/server/models/base/plugins/actions.js +26 -3
  98. package/core/server/models/member-created-event.js +10 -2
  99. package/core/server/models/member-paid-subscription-event.js +4 -0
  100. package/core/server/models/member.js +18 -0
  101. package/core/server/models/offer.js +3 -0
  102. package/core/server/models/post.js +2 -3
  103. package/core/server/models/product.js +3 -0
  104. package/core/server/models/settings.js +4 -0
  105. package/core/server/models/subscription-created-event.js +10 -2
  106. package/core/server/models/user.js +41 -7
  107. package/core/server/services/auth/api-key/admin.js +0 -3
  108. package/core/server/services/auth/passwordreset.js +0 -3
  109. package/core/server/services/explore/service.js +7 -6
  110. package/core/server/services/mega/mega.js +7 -4
  111. package/core/server/services/mega/template.js +44 -16
  112. package/core/server/services/member-attribution/index.js +34 -6
  113. package/core/server/services/members/api.js +4 -0
  114. package/core/server/services/members/middleware.js +6 -2
  115. package/core/server/services/members/service.js +6 -3
  116. package/core/server/services/public-config/site.js +1 -0
  117. package/core/server/services/route-settings/default-settings-manager.js +19 -17
  118. package/core/server/services/staff/index.js +26 -0
  119. package/core/server/services/webhooks/trigger.js +14 -5
  120. package/core/server/web/api/endpoints/admin/middleware.js +1 -3
  121. package/core/server/web/api/endpoints/admin/routes.js +0 -7
  122. package/core/shared/config/defaults.json +3 -2
  123. package/core/shared/labs.js +8 -7
  124. package/package.json +86 -84
  125. package/yarn.lock +134 -181
  126. package/components/tryghost-api-framework-5.10.1.tgz +0 -0
  127. package/components/tryghost-api-version-compatibility-service-5.10.1.tgz +0 -0
  128. package/components/tryghost-constants-5.10.1.tgz +0 -0
  129. package/components/tryghost-domain-events-5.10.1.tgz +0 -0
  130. package/components/tryghost-express-dynamic-redirects-5.10.1.tgz +0 -0
  131. package/components/tryghost-extract-api-key-5.10.1.tgz +0 -0
  132. package/components/tryghost-magic-link-5.10.1.tgz +0 -0
  133. package/components/tryghost-mailgun-client-5.10.1.tgz +0 -0
  134. package/components/tryghost-member-analytics-service-5.10.1.tgz +0 -0
  135. package/components/tryghost-member-attribution-5.10.1.tgz +0 -0
  136. package/components/tryghost-member-events-5.10.1.tgz +0 -0
  137. package/components/tryghost-members-api-5.10.1.tgz +0 -0
  138. package/components/tryghost-members-importer-5.10.1.tgz +0 -0
  139. package/components/tryghost-members-offers-5.10.1.tgz +0 -0
  140. package/components/tryghost-members-ssr-5.10.1.tgz +0 -0
  141. package/components/tryghost-minifier-5.10.1.tgz +0 -0
  142. package/components/tryghost-mw-api-version-mismatch-5.10.1.tgz +0 -0
  143. package/components/tryghost-mw-error-handler-5.10.1.tgz +0 -0
  144. package/components/tryghost-mw-update-user-last-seen-5.10.1.tgz +0 -0
  145. package/components/tryghost-mw-vhost-5.10.1.tgz +0 -0
  146. package/components/tryghost-settings-path-manager-5.10.1.tgz +0 -0
  147. package/components/tryghost-verification-trigger-5.10.1.tgz +0 -0
  148. package/core/built/admin/assets/chunk.143.71da0e884297554a8ec4.js +0 -41
  149. package/core/built/admin/assets/ghost-13baab17b3f54b21f341fb8f36f83110.css +0 -1
  150. package/core/built/admin/assets/ghost-dark-b0500577a42e2770994e6aef0e70f182.css +0 -1
@@ -306,10 +306,11 @@ async function sendEmailJob({emailModel, options}) {
306
306
  const existingBatchCount = await emailModel.related('emailBatches').count('id');
307
307
 
308
308
  if (existingBatchCount === 0) {
309
- let newBatchCount;
309
+ let newBatchCount = 0;
310
310
 
311
311
  await models.Base.transaction(async (transacting) => {
312
- newBatchCount = await createSegmentedEmailBatches({emailModel, options: {transacting}});
312
+ const emailBatches = await createSegmentedEmailBatches({emailModel, options: {transacting}});
313
+ newBatchCount = emailBatches.length;
313
314
  });
314
315
 
315
316
  if (newBatchCount === 0) {
@@ -423,6 +424,8 @@ function partitionMembersBySegment(memberRows, segments) {
423
424
  * @param {Object} options
424
425
  * @param {Object} options.emailModel - instance of Email model
425
426
  * @param {Object} options.options - knex options
427
+ *
428
+ * @returns {Promise<string[]>}
426
429
  */
427
430
  async function createSegmentedEmailBatches({emailModel, options}) {
428
431
  let memberRows = await getEmailMemberRows({emailModel, options});
@@ -444,11 +447,11 @@ async function createSegmentedEmailBatches({emailModel, options}) {
444
447
  memberSegment: partition === 'unsegmented' ? null : partition,
445
448
  options
446
449
  });
447
- batchIds.push(emailBatchIds);
450
+ batchIds.push(...emailBatchIds);
448
451
  }
449
452
  } else {
450
453
  const emailBatchIds = await createEmailBatches({emailModel, memberRows, options});
451
- batchIds.push(emailBatchIds);
454
+ batchIds.push(...emailBatchIds);
452
455
  }
453
456
 
454
457
  return batchIds;
@@ -1,15 +1,43 @@
1
1
  /* eslint indent: warn, no-irregular-whitespace: warn */
2
2
  const iff = (cond, yes, no) => (cond ? yes : no);
3
+ const sanitizeHtml = require('sanitize-html');
4
+
5
+ /**
6
+ * @template {Object.<string, any>} Input
7
+ * @param {Input} obj
8
+ * @param {string[]} [keys]
9
+ * @returns {Input}
10
+ */
11
+ const sanitizeKeys = (obj, keys) => {
12
+ const sanitized = Object.assign({}, obj);
13
+ const keysToSanitize = keys || Object.keys(obj);
14
+
15
+ for (const key of keysToSanitize) {
16
+ if (typeof sanitized[key] === 'string') {
17
+ // @ts-ignore
18
+ sanitized[key] = sanitizeHtml(sanitized[key], {
19
+ allowedTags: false,
20
+ allowedAttributes: false
21
+ });
22
+ }
23
+ }
24
+
25
+ return sanitized;
26
+ };
27
+
3
28
  module.exports = ({post, site, newsletter, templateSettings}) => {
4
29
  const date = new Date();
5
30
  const hasFeatureImageCaption = templateSettings.showFeatureImage && post.feature_image && post.feature_image_caption;
31
+ const cleanPost = sanitizeKeys(post, ['title', 'excerpt', 'html', 'feature_image_alt', 'feature_image_caption']);
32
+ const cleanSite = sanitizeKeys(site, ['title']);
33
+ const cleanNewsletter = sanitizeKeys(newsletter, ['name']);
6
34
  return `<!doctype html>
7
35
  <html>
8
36
 
9
37
  <head>
10
38
  <meta name="viewport" content="width=device-width" />
11
39
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
12
- <title>${post.title}</title>
40
+ <title>${cleanPost.title}</title>
13
41
  <style>
14
42
  /* -------------------------------------
15
43
  GLOBAL RESETS
@@ -1137,7 +1165,7 @@ ${ templateSettings.showBadge ? `
1137
1165
  </head>
1138
1166
 
1139
1167
  <body>
1140
- <span class="preheader">${ post.excerpt ? post.excerpt : `${post.title} – ` }</span>
1168
+ <span class="preheader">${ cleanPost.excerpt ? cleanPost.excerpt : `${cleanPost.title} – ` }</span>
1141
1169
  <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" width="100%">
1142
1170
 
1143
1171
  <!-- Outlook doesn't respect max-width so we need an extra centered table -->
@@ -1172,24 +1200,24 @@ ${ templateSettings.showBadge ? `
1172
1200
  <tr>
1173
1201
  <td class="${templateSettings.showHeaderTitle ? `site-info-bordered` : `site-info`}" width="100%" align="center">
1174
1202
  <table role="presentation" border="0" cellpadding="0" cellspacing="0">
1175
- ${ templateSettings.showHeaderIcon && site.iconUrl ? `
1203
+ ${ templateSettings.showHeaderIcon && cleanSite.iconUrl ? `
1176
1204
  <tr>
1177
- <td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" alt="${site.title}" border="0"></a></td>
1205
+ <td class="site-icon"><a href="${cleanSite.url}"><img src="${cleanSite.iconUrl}" alt="${cleanSite.title}" border="0"></a></td>
1178
1206
  </tr>
1179
1207
  ` : ``}
1180
1208
  ${ templateSettings.showHeaderTitle ? `
1181
1209
  <tr>
1182
- <td class="site-url ${!templateSettings.showHeaderName ? 'site-url-bottom-padding' : ''}"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${site.title}</a></div></td>
1210
+ <td class="site-url ${!templateSettings.showHeaderName ? 'site-url-bottom-padding' : ''}"><div style="width: 100% !important;"><a href="${cleanSite.url}" class="site-title">${cleanSite.title}</a></div></td>
1183
1211
  </tr>
1184
1212
  ` : ``}
1185
1213
  ${ templateSettings.showHeaderName && templateSettings.showHeaderTitle ? `
1186
1214
  <tr>
1187
- <td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${site.url}" class="site-subtitle">${newsletter.name}</a></div></td>
1215
+ <td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${cleanSite.url}" class="site-subtitle">${cleanNewsletter.name}</a></div></td>
1188
1216
  </tr>
1189
1217
  ` : ``}
1190
1218
  ${ templateSettings.showHeaderName && !templateSettings.showHeaderTitle ? `
1191
1219
  <tr>
1192
- <td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${newsletter.name}</a></div></td>
1220
+ <td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${cleanSite.url}" class="site-title">${cleanNewsletter.name}</a></div></td>
1193
1221
  </tr>
1194
1222
  ` : ``}
1195
1223
 
@@ -1201,7 +1229,7 @@ ${ templateSettings.showBadge ? `
1201
1229
 
1202
1230
  <tr>
1203
1231
  <td class="post-title ${templateSettings.titleFontCategory === 'serif' ? `post-title-serif` : `` } ${templateSettings.titleAlignment === 'left' ? `post-title-left` : ``}">
1204
- <a href="${post.url}" class="post-title-link ${templateSettings.titleAlignment === 'left' ? `post-title-link-left` : ``}">${post.title}</a>
1232
+ <a href="${cleanPost.url}" class="post-title-link ${templateSettings.titleAlignment === 'left' ? `post-title-link-left` : ``}">${cleanPost.title}</a>
1205
1233
  </td>
1206
1234
  </tr>
1207
1235
  <tr>
@@ -1209,28 +1237,28 @@ ${ templateSettings.showBadge ? `
1209
1237
  <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
1210
1238
  <tr>
1211
1239
  <td class="post-meta ${templateSettings.titleAlignment === 'left' ? `post-meta-left` : ``}">
1212
- By ${post.authors} –
1213
- ${post.published_at} –
1214
- <a href="${post.url}" class="view-online-link">View online →</a>
1240
+ By ${cleanPost.authors} –
1241
+ ${cleanPost.published_at} –
1242
+ <a href="${cleanPost.url}" class="view-online-link">View online →</a>
1215
1243
  </td>
1216
1244
  </tr>
1217
1245
  </table>
1218
1246
  </td>
1219
1247
  </tr>
1220
- ${ templateSettings.showFeatureImage && post.feature_image ? `
1248
+ ${ templateSettings.showFeatureImage && cleanPost.feature_image ? `
1221
1249
  <tr>
1222
- <td class="feature-image ${hasFeatureImageCaption ? 'feature-image-with-caption' : ''}"><img src="${post.feature_image}"${post.feature_image_width ? ` width="${post.feature_image_width}"` : ''}${post.feature_image_alt ? ` alt="${post.feature_image_alt}"` : ''}></td>
1250
+ <td class="feature-image ${hasFeatureImageCaption ? 'feature-image-with-caption' : ''}"><img src="${cleanPost.feature_image}"${cleanPost.feature_image_width ? ` width="${cleanPost.feature_image_width}"` : ''}${cleanPost.feature_image_alt ? ` alt="${cleanPost.feature_image_alt}"` : ''}></td>
1223
1251
  </tr>
1224
1252
  ` : ``}
1225
1253
  ${ hasFeatureImageCaption ? `
1226
1254
  <tr>
1227
- <td class="feature-image-caption" align="center">${post.feature_image_caption}</td>
1255
+ <td class="feature-image-caption" align="center">${cleanPost.feature_image_caption}</td>
1228
1256
  </tr>
1229
1257
  ` : ``}
1230
1258
  <tr>
1231
1259
  <td class="${(templateSettings.bodyFontCategory === 'sans_serif') ? `post-content-sans-serif` : `post-content` }">
1232
1260
  <!-- POST CONTENT START -->
1233
- ${post.html}
1261
+ ${cleanPost.html}
1234
1262
  <!-- POST CONTENT END -->
1235
1263
  </td>
1236
1264
  </tr>
@@ -1245,7 +1273,7 @@ ${ templateSettings.showBadge ? `
1245
1273
  <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-top: 40px; padding-bottom: 30px;">
1246
1274
  ${iff(!!templateSettings.footerContent, `<tr><td class="footer">${templateSettings.footerContent}</td></tr>`, '')}
1247
1275
  <tr>
1248
- <td class="footer">${site.title} &copy; ${date.getFullYear()} – <a href="%recipient.unsubscribe_url%">Unsubscribe</a></td>
1276
+ <td class="footer">${cleanSite.title} &copy; ${date.getFullYear()} – <a href="%recipient.unsubscribe_url%">Unsubscribe</a></td>
1249
1277
  </tr>
1250
1278
 
1251
1279
  ${ templateSettings.showBadge ? `
@@ -1,23 +1,51 @@
1
1
  const urlService = require('../url');
2
2
  const labsService = require('../../../shared/labs');
3
+ const DomainEvents = require('@tryghost/domain-events');
4
+ const urlUtils = require('../../../shared/url-utils');
3
5
 
4
6
  class MemberAttributionServiceWrapper {
5
7
  init() {
6
- if (this.service) {
8
+ if (this.eventHandler) {
7
9
  // Prevent creating duplicate DomainEvents subscribers
8
10
  return;
9
11
  }
10
12
 
11
- const MemberAttributionService = require('@tryghost/member-attribution');
13
+ // Wire up all the dependencies
14
+ const {MemberAttributionService, UrlTranslator, AttributionBuilder, EventHandler} = require('@tryghost/member-attribution');
12
15
  const models = require('../../models');
13
16
 
14
- // For now we don't need to expose anything (yet)
17
+ const urlTranslator = new UrlTranslator({
18
+ urlService,
19
+ urlUtils,
20
+ models: {
21
+ Post: models.Post,
22
+ User: models.User,
23
+ Tag: models.Tag
24
+ }
25
+ });
26
+
27
+ this.attributionBuilder = new AttributionBuilder({urlTranslator});
28
+
29
+ // Expose the service
15
30
  this.service = new MemberAttributionService({
16
- MemberCreatedEvent: models.MemberCreatedEvent,
17
- SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
18
- urlService,
31
+ models: {
32
+ MemberCreatedEvent: models.MemberCreatedEvent,
33
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
34
+ },
35
+ attributionBuilder: this.attributionBuilder,
36
+ labsService
37
+ });
38
+
39
+ // Listen for events and store them in the database
40
+ this.eventHandler = new EventHandler({
41
+ models: {
42
+ MemberCreatedEvent: models.MemberCreatedEvent,
43
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent
44
+ },
45
+ DomainEvents,
19
46
  labsService
20
47
  });
48
+ this.eventHandler.subscribe();
21
49
  }
22
50
  }
23
51
 
@@ -13,6 +13,7 @@ const SingleUseTokenProvider = require('./SingleUseTokenProvider');
13
13
  const urlUtils = require('../../../shared/url-utils');
14
14
  const labsService = require('../../../shared/labs');
15
15
  const offersService = require('../offers');
16
+ const staffService = require('../staff');
16
17
  const newslettersService = require('../newsletters');
17
18
  const memberAttributionService = require('../member-attribution');
18
19
 
@@ -185,6 +186,8 @@ function createApiInstance(config) {
185
186
  MemberStatusEvent: models.MemberStatusEvent,
186
187
  MemberProductEvent: models.MemberProductEvent,
187
188
  MemberAnalyticEvent: models.MemberAnalyticEvent,
189
+ MemberCreatedEvent: models.MemberCreatedEvent,
190
+ SubscriptionCreatedEvent: models.SubscriptionCreatedEvent,
188
191
  OfferRedemption: models.OfferRedemption,
189
192
  Offer: models.Offer,
190
193
  StripeProduct: models.StripeProduct,
@@ -195,6 +198,7 @@ function createApiInstance(config) {
195
198
  },
196
199
  stripeAPIService: stripeService.api,
197
200
  offersAPI: offersService.api,
201
+ staffService: staffService.api,
198
202
  labsService: labsService,
199
203
  newslettersService: newslettersService,
200
204
  memberAttributionService: memberAttributionService.service
@@ -36,7 +36,9 @@ const deleteSession = async function (req, res) {
36
36
  res.writeHead(204);
37
37
  res.end();
38
38
  } catch (err) {
39
- res.writeHead(err.statusCode);
39
+ res.writeHead(err.statusCode, {
40
+ 'Content-Type': 'text/plain;charset=UTF-8'
41
+ });
40
42
  res.end(err.message);
41
43
  }
42
44
  };
@@ -130,7 +132,9 @@ const updateMemberData = async function (req, res) {
130
132
  res.json(null);
131
133
  }
132
134
  } catch (err) {
133
- res.writeHead(err.statusCode);
135
+ res.writeHead(err.statusCode, {
136
+ 'Content-Type': 'text/plain;charset=UTF-8'
137
+ });
134
138
  res.end(err.message);
135
139
  }
136
140
  };
@@ -105,10 +105,12 @@ module.exports = {
105
105
  });
106
106
 
107
107
  verificationTrigger = new VerificationTrigger({
108
- configThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
108
+ apiTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.apiThreshold'),
109
+ adminTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.adminThreshold'),
110
+ importTriggerThreshold: _.get(config.get('hostSettings'), 'emailVerification.importThreshold'),
109
111
  isVerified: () => config.get('hostSettings:emailVerification:verified') === true,
110
112
  isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
111
- sendVerificationEmail: ({subject, message, amountImported}) => {
113
+ sendVerificationEmail: ({subject, message, amountTriggered}) => {
112
114
  const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
113
115
  const fromAddress = config.get('user_email');
114
116
 
@@ -116,7 +118,7 @@ module.exports = {
116
118
  ghostMailer.send({
117
119
  subject,
118
120
  html: tpl(message, {
119
- importedNumber: amountImported,
121
+ amountTriggered: amountTriggered,
120
122
  siteUrl: urlUtils.getSiteUrl()
121
123
  }),
122
124
  forceTextContent: true,
@@ -189,4 +191,5 @@ module.exports = {
189
191
  stats: membersStats,
190
192
  export: require('./exporter/query')
191
193
  };
194
+
192
195
  module.exports.middleware = require('./middleware');
@@ -10,6 +10,7 @@ module.exports = function getSiteProperties() {
10
10
  logo: settingsCache.get('logo'),
11
11
  icon: settingsCache.get('icon'),
12
12
  accent_color: settingsCache.get('accent_color'),
13
+ locale: settingsCache.get('locale'),
13
14
  url: urlUtils.urlFor('home', true),
14
15
  version: ghostVersion.safe
15
16
  };
@@ -38,23 +38,25 @@ class DefaultSettingsManager {
38
38
  const defaultFilePath = path.join(this.sourceFolderPath, defaultFileName);
39
39
 
40
40
  return Promise.resolve(fs.readFile(destinationFilePath, 'utf8'))
41
- .catch({code: 'ENOENT'}, () => {
42
- // CASE: file doesn't exist, copy it from our defaults
43
- return fs.copy(
44
- defaultFilePath,
45
- destinationFilePath
46
- ).then(() => {
47
- debug(`'${defaultFileName}' copied to ${this.destinationFolderPath}.`);
48
- });
49
- }).catch((error) => {
50
- // CASE: we might have a permission error, as we can't access the directory
51
- throw new errors.InternalServerError({
52
- message: tpl(messages.ensureSettings, {
53
- path: this.destinationFolderPath
54
- }),
55
- err: error,
56
- context: error.path
57
- });
41
+ .catch((err) => {
42
+ if (err.code === 'ENOENT') {
43
+ // CASE: file doesn't exist, copy it from our defaults
44
+ return fs.copy(
45
+ defaultFilePath,
46
+ destinationFilePath
47
+ ).then(() => {
48
+ debug(`'${defaultFileName}' copied to ${this.destinationFolderPath}.`);
49
+ });
50
+ } else {
51
+ // CASE: we might have a permission error, as we can't access the directory
52
+ throw new errors.InternalServerError({
53
+ message: tpl(messages.ensureSettings, {
54
+ path: this.destinationFolderPath
55
+ }),
56
+ err: err,
57
+ context: err.path
58
+ });
59
+ }
58
60
  });
59
61
  }
60
62
  }
@@ -0,0 +1,26 @@
1
+ class StaffServiceWrapper {
2
+ init() {
3
+ const StaffService = require('@tryghost/staff-service');
4
+
5
+ const config = require('../../../shared/config');
6
+ const logging = require('@tryghost/logging');
7
+ const models = require('../../models');
8
+ const {GhostMailer} = require('../mail');
9
+ const mailer = new GhostMailer();
10
+ const settingsCache = require('../../../shared/settings-cache');
11
+ const urlService = require('../url');
12
+ const urlUtils = require('../../../shared/url-utils');
13
+
14
+ this.api = new StaffService({
15
+ config,
16
+ logging,
17
+ models,
18
+ mailer,
19
+ settingsCache,
20
+ urlService,
21
+ urlUtils
22
+ });
23
+ }
24
+ }
25
+
26
+ module.exports = new StaffServiceWrapper();
@@ -1,6 +1,7 @@
1
1
  const debug = require('@tryghost/debug')('services:webhooks:trigger');
2
2
  const logging = require('@tryghost/logging');
3
3
  const ghostVersion = require('@tryghost/version');
4
+ const crypto = require('crypto');
4
5
 
5
6
  class WebhookTrigger {
6
7
  /**
@@ -85,13 +86,21 @@ class WebhookTrigger {
85
86
 
86
87
  const reqPayload = JSON.stringify(hookPayload);
87
88
  const url = webhook.get('target_url');
89
+ const secret = webhook.get('secret') || '';
90
+
91
+ const headers = {
92
+ 'Content-Length': Buffer.byteLength(reqPayload),
93
+ 'Content-Type': 'application/json',
94
+ 'Content-Version': `v${ghostVersion.safe}`
95
+ };
96
+
97
+ if (secret !== '') {
98
+ headers['X-Ghost-Signature'] = `sha256=${crypto.createHmac('sha256', secret).update(reqPayload).digest('hex')}, t=${Date.now()}`;
99
+ }
100
+
88
101
  const opts = {
89
102
  body: reqPayload,
90
- headers: {
91
- 'Content-Length': Buffer.byteLength(reqPayload),
92
- 'Content-Type': 'application/json',
93
- 'Content-Version': `v${ghostVersion.safe}`
94
- },
103
+ headers,
95
104
  timeout: 2 * 1000,
96
105
  retry: 5
97
106
  };
@@ -14,15 +14,13 @@ const notImplemented = function (req, res, next) {
14
14
  return next();
15
15
  }
16
16
 
17
- // @NOTE: integrations have limited access for now
17
+ // @NOTE: integrations & staff tokens have limited access to the API
18
18
  const allowlisted = {
19
- // @NOTE: stable
20
19
  site: ['GET'],
21
20
  posts: ['GET', 'PUT', 'DELETE', 'POST'],
22
21
  pages: ['GET', 'PUT', 'DELETE', 'POST'],
23
22
  images: ['POST'],
24
23
  webhooks: ['POST', 'PUT', 'DELETE'],
25
- // @NOTE: experimental
26
24
  actions: ['GET'],
27
25
  tags: ['GET', 'PUT', 'DELETE', 'POST'],
28
26
  labels: ['GET', 'PUT', 'DELETE', 'POST'],
@@ -65,13 +65,6 @@ module.exports = function apiRoutes() {
65
65
  router.get('/settings', mw.authAdminApi, http(api.settings.browse));
66
66
  router.put('/settings', mw.authAdminApi, http(api.settings.edit));
67
67
  router.put('/settings/verifications/', mw.authAdminApi, http(api.settings.verifyKeyUpdate));
68
-
69
- /** @deprecated This endpoint is part of the old email verification flow for the support email */
70
- router.get('/settings/members/email', http(api.settings.validateMembersEmailUpdate));
71
-
72
- /** @deprecated This endpoint is part of the old email verification flow for the support email */
73
- router.post('/settings/members/email', mw.authAdminApi, http(api.settings.updateMembersEmail));
74
-
75
68
  router.del('/settings/stripe/connect', mw.authAdminApi, http(api.settings.disconnectStripeConnectIntegration));
76
69
 
77
70
  // ## Users
@@ -141,7 +141,7 @@
141
141
  },
142
142
  "portal": {
143
143
  "url": "https://cdn.jsdelivr.net/npm/@tryghost/portal@~{version}/umd/portal.min.js",
144
- "version": "2.9"
144
+ "version": "2.11"
145
145
  },
146
146
  "sodoSearch": {
147
147
  "url": "https://cdn.jsdelivr.net/npm/@tryghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -154,7 +154,8 @@
154
154
  "version": "0.9"
155
155
  },
156
156
  "editor": {
157
- "url": "https://unpkg.com/@tryghost/koenig-react/dist/umd/koenig-react.min.js"
157
+ "url": "https://unpkg.com/@tryghost/koenig-react/dist/umd/koenig-react.min.js",
158
+ "lexicalUrl": "https://unpkg.com/@tryghost/koenig-lexical-experiment/dist/koenig-lexical.umd.js"
158
159
  },
159
160
  "tenor": {
160
161
  "googleApiKey": null,
@@ -15,23 +15,24 @@ const messages = {
15
15
 
16
16
  // flags in this list always return `true`, allows quick global enable prior to full flag removal
17
17
  const GA_FEATURES = [
18
- 'newsletterPaywall'
18
+ 'newsletterPaywall',
19
+ 'freeTrial',
20
+ 'compExpiring',
21
+ 'searchHelper',
22
+ 'emailAlerts'
19
23
  ];
20
24
 
21
25
  // NOTE: this allowlist is meant to be used to filter out any unexpected
22
26
  // input for the "labs" setting value
23
27
  const BETA_FEATURES = [
24
- 'activitypub'
28
+ 'activitypub',
29
+ 'memberAttribution'
25
30
  ];
26
31
 
27
32
  const ALPHA_FEATURES = [
28
33
  'auditLog',
29
34
  'urlCache',
30
- 'beforeAfterCard',
31
- 'freeTrial',
32
- 'memberAttribution',
33
- 'searchHelper',
34
- 'compExpiring'
35
+ 'beforeAfterCard'
35
36
  ];
36
37
 
37
38
  module.exports.GA_KEYS = [...GA_FEATURES];