ghost 5.14.1 → 5.15.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 (149) hide show
  1. package/components/{tryghost-adapter-manager-5.14.1.tgz → tryghost-adapter-manager-5.15.0.tgz} +0 -0
  2. package/components/{tryghost-api-framework-5.14.1.tgz → tryghost-api-framework-5.15.0.tgz} +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-5.14.1.tgz → tryghost-api-version-compatibility-service-5.15.0.tgz} +0 -0
  4. package/components/tryghost-bootstrap-socket-5.15.0.tgz +0 -0
  5. package/components/tryghost-constants-5.15.0.tgz +0 -0
  6. package/components/{tryghost-custom-theme-settings-service-5.14.1.tgz → tryghost-custom-theme-settings-service-5.15.0.tgz} +0 -0
  7. package/components/tryghost-domain-events-5.15.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.15.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-service-5.15.0.tgz +0 -0
  10. package/components/tryghost-email-content-generator-5.15.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.15.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.15.0.tgz +0 -0
  13. package/components/tryghost-html-to-plaintext-5.15.0.tgz +0 -0
  14. package/components/{tryghost-job-manager-5.14.1.tgz → tryghost-job-manager-5.15.0.tgz} +0 -0
  15. package/components/tryghost-link-redirects-5.15.0.tgz +0 -0
  16. package/components/tryghost-link-replacement-5.15.0.tgz +0 -0
  17. package/components/tryghost-link-tracking-5.15.0.tgz +0 -0
  18. package/components/{tryghost-magic-link-5.14.1.tgz → tryghost-magic-link-5.15.0.tgz} +0 -0
  19. package/components/tryghost-mailgun-client-5.15.0.tgz +0 -0
  20. package/components/{tryghost-member-analytics-service-5.14.1.tgz → tryghost-member-analytics-service-5.15.0.tgz} +0 -0
  21. package/components/tryghost-member-attribution-5.15.0.tgz +0 -0
  22. package/components/tryghost-member-events-5.15.0.tgz +0 -0
  23. package/components/tryghost-members-analytics-ingress-5.15.0.tgz +0 -0
  24. package/components/tryghost-members-api-5.15.0.tgz +0 -0
  25. package/components/tryghost-members-csv-5.15.0.tgz +0 -0
  26. package/components/tryghost-members-events-service-5.15.0.tgz +0 -0
  27. package/components/tryghost-members-importer-5.15.0.tgz +0 -0
  28. package/components/tryghost-members-offers-5.15.0.tgz +0 -0
  29. package/components/{tryghost-members-payments-5.14.1.tgz → tryghost-members-payments-5.15.0.tgz} +0 -0
  30. package/components/{tryghost-members-ssr-5.14.1.tgz → tryghost-members-ssr-5.15.0.tgz} +0 -0
  31. package/components/tryghost-members-stripe-service-5.15.0.tgz +0 -0
  32. package/components/tryghost-minifier-5.15.0.tgz +0 -0
  33. package/components/tryghost-mw-api-version-mismatch-5.15.0.tgz +0 -0
  34. package/components/tryghost-mw-cache-control-5.15.0.tgz +0 -0
  35. package/components/{tryghost-mw-error-handler-5.14.1.tgz → tryghost-mw-error-handler-5.15.0.tgz} +0 -0
  36. package/components/tryghost-mw-session-from-token-5.15.0.tgz +0 -0
  37. package/components/tryghost-mw-update-user-last-seen-5.15.0.tgz +0 -0
  38. package/components/tryghost-mw-vhost-5.15.0.tgz +0 -0
  39. package/components/{tryghost-oembed-service-5.14.1.tgz → tryghost-oembed-service-5.15.0.tgz} +0 -0
  40. package/components/{tryghost-package-json-5.14.1.tgz → tryghost-package-json-5.15.0.tgz} +0 -0
  41. package/components/tryghost-security-5.15.0.tgz +0 -0
  42. package/components/{tryghost-session-service-5.14.1.tgz → tryghost-session-service-5.15.0.tgz} +0 -0
  43. package/components/{tryghost-settings-path-manager-5.14.1.tgz → tryghost-settings-path-manager-5.15.0.tgz} +0 -0
  44. package/components/tryghost-staff-service-5.15.0.tgz +0 -0
  45. package/components/{tryghost-update-check-service-5.14.1.tgz → tryghost-update-check-service-5.15.0.tgz} +0 -0
  46. package/components/{tryghost-verification-trigger-5.14.1.tgz → tryghost-verification-trigger-5.15.0.tgz} +0 -0
  47. package/components/tryghost-version-notifications-data-service-5.15.0.tgz +0 -0
  48. package/content/themes/casper/default.hbs +2 -2
  49. package/core/boot.js +12 -3
  50. package/core/built/admin/assets/{chunk.143.fa1d2a279fd46b1f8e63.js → chunk.143.558b9943af7b15f189ae.js} +20 -20
  51. package/core/built/admin/assets/{chunk.174.2edaa0869bfc2d88cf90.js → chunk.174.e1e89637eab79fdd5c5d.js} +68 -68
  52. package/core/built/admin/assets/{chunk.178.2292bd0899558aebead6.js → chunk.178.a9f6ddaea01e2bc76235.js} +4 -4
  53. package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js → chunk.579.dc11bf8dda5cf4406708.js} +5464 -4961
  54. package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js.LICENSE.txt → chunk.579.dc11bf8dda5cf4406708.js.LICENSE.txt} +0 -0
  55. package/core/built/admin/assets/fonts/{Inter.ttf → Inter-e19174fb2c0e19b1fa67492a07886c75.ttf} +0 -0
  56. package/core/built/admin/assets/{ghost-8919656440ad4617a07bb31069b1f71b.js → ghost-4b1b550e34300f5f4774a261aac29557.js} +487 -470
  57. package/core/built/admin/assets/ghost-c933adafb359b75ea1577365ce252e76.css +1 -0
  58. package/core/built/admin/assets/ghost-dark-04981c84bf590e0fae0a8e83e018190f.css +1 -0
  59. package/core/built/admin/assets/img/{amp.svg → amp-d7b72aae3315fda95921fb575dfca100.svg} +0 -0
  60. package/core/built/admin/assets/img/{disqus.svg → disqus-43503a3fa4f38dc8c61c7358b811f343.svg} +0 -0
  61. package/core/built/admin/assets/img/{favicon.ico → favicon-a9c6dbdcdc3ae568f4e0dad92149a0e3.ico} +0 -0
  62. package/core/built/admin/assets/img/{github.svg → github-c3a739c59df26fed12c10ffb00b33bd4.svg} +0 -0
  63. package/core/built/admin/assets/img/{google-docs.svg → google-docs-1e42cc272fc088da49e4b0ddfb01b006.svg} +0 -0
  64. package/core/built/admin/assets/img/{mailchimp.svg → mailchimp-f22b1e130aac764965b9306d7265a6b2.svg} +0 -0
  65. package/core/built/admin/assets/img/{patreon.svg → patreon-b19a5e6418a72977a16b30039d374d04.svg} +0 -0
  66. package/core/built/admin/assets/img/{paypal.svg → paypal-38e9448ce7549ea4caf8e7753ae661d6.svg} +0 -0
  67. package/core/built/admin/assets/img/{twitter.svg → twitter-7a7a0ba12d9b5bfb8a2058764a827c31.svg} +0 -0
  68. package/core/built/admin/assets/img/{typeform.svg → typeform-9f23f8712d776a7515594676285266f5.svg} +0 -0
  69. package/core/built/admin/assets/img/{unsplash.svg → unsplash-5b329eef0b11447b4117eaf817ebad6f.svg} +0 -0
  70. package/core/built/admin/assets/img/{zapier.svg → zapier-bf93bc440a3fd43b73489a63c215cdc7.svg} +0 -0
  71. package/core/built/admin/assets/img/{zapier-logo.svg → zapier-logo-a125f24313dfe01ef49af01fc90061fb.svg} +0 -0
  72. package/core/built/admin/assets/{vendor-eb76d0236a09b8b6f44675dba45b1fc6.js → vendor-271c32988ab16ba175a9bfa2acb2887a.js} +45 -39
  73. package/core/built/admin/assets/videos/logo-loader.mp4 +0 -0
  74. package/core/built/admin/index.html +11 -8
  75. package/core/frontend/src/member-attribution/member-attribution.js +27 -0
  76. package/core/frontend/web/site.js +10 -7
  77. package/core/server/api/endpoints/redirects.js +6 -8
  78. package/core/server/api/endpoints/utils/permissions.js +2 -16
  79. package/core/server/api/endpoints/utils/serializers/input/pages.js +5 -5
  80. package/core/server/api/endpoints/utils/serializers/input/posts.js +7 -7
  81. package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
  82. package/core/server/api/endpoints/utils/validators/input/pages.js +24 -9
  83. package/core/server/api/endpoints/utils/validators/input/posts.js +24 -9
  84. package/core/server/data/exporter/table-lists.js +1 -0
  85. package/core/server/data/migrations/utils/settings.js +1 -3
  86. package/core/server/data/migrations/versions/5.15/2022-09-12-16-10-add-posts-lexical-column.js +8 -0
  87. package/core/server/data/migrations/versions/5.15/2022-09-14-12-46-add-email-track-clicks-setting.js +8 -0
  88. package/core/server/data/migrations/versions/5.15/2022-09-16-08-22-add-post-revisions-table.js +9 -0
  89. package/core/server/data/schema/default-settings/default-settings.json +8 -0
  90. package/core/server/data/schema/schema.js +8 -0
  91. package/core/server/lib/lexical.js +12 -0
  92. package/core/server/models/base/plugins/user-type.js +4 -6
  93. package/core/server/models/post-revision.js +35 -0
  94. package/core/server/models/post.js +72 -7
  95. package/core/server/services/bulk-email/bulk-email-processor.js +2 -5
  96. package/core/server/services/{redirects → custom-redirects}/api.js +0 -0
  97. package/core/server/services/{redirects → custom-redirects}/index.js +0 -0
  98. package/core/server/services/{redirects → custom-redirects}/utils.js +0 -0
  99. package/core/server/services/{redirects → custom-redirects}/validation.js +0 -0
  100. package/core/server/services/explore/service.js +5 -3
  101. package/core/server/services/link-click-tracking/index.js +25 -0
  102. package/core/server/services/link-redirection/index.js +33 -0
  103. package/core/server/services/link-replacement/index.js +24 -0
  104. package/core/server/services/mega/email-preview.js +7 -0
  105. package/core/server/services/mega/mega.js +1 -1
  106. package/core/server/services/mega/post-email-serializer.js +75 -27
  107. package/core/server/services/mega/template.js +1 -1
  108. package/core/server/services/members/api.js +0 -2
  109. package/core/server/services/permissions/index.js +1 -2
  110. package/core/server/services/posts/posts-service.js +7 -16
  111. package/core/server/services/posts/stats/post-stats.js +35 -0
  112. package/core/server/services/staff/index.js +10 -1
  113. package/core/server/services/url/config.js +2 -0
  114. package/core/shared/config/defaults.json +2 -2
  115. package/core/shared/config/overrides.json +3 -2
  116. package/core/shared/labs.js +4 -2
  117. package/package.json +97 -90
  118. package/yarn.lock +395 -198
  119. package/components/tryghost-bootstrap-socket-5.14.1.tgz +0 -0
  120. package/components/tryghost-constants-5.14.1.tgz +0 -0
  121. package/components/tryghost-domain-events-5.14.1.tgz +0 -0
  122. package/components/tryghost-email-analytics-provider-mailgun-5.14.1.tgz +0 -0
  123. package/components/tryghost-email-analytics-service-5.14.1.tgz +0 -0
  124. package/components/tryghost-email-content-generator-5.14.1.tgz +0 -0
  125. package/components/tryghost-express-dynamic-redirects-5.14.1.tgz +0 -0
  126. package/components/tryghost-extract-api-key-5.14.1.tgz +0 -0
  127. package/components/tryghost-html-to-plaintext-5.14.1.tgz +0 -0
  128. package/components/tryghost-mailgun-client-5.14.1.tgz +0 -0
  129. package/components/tryghost-member-attribution-5.14.1.tgz +0 -0
  130. package/components/tryghost-member-events-5.14.1.tgz +0 -0
  131. package/components/tryghost-members-analytics-ingress-5.14.1.tgz +0 -0
  132. package/components/tryghost-members-api-5.14.1.tgz +0 -0
  133. package/components/tryghost-members-csv-5.14.1.tgz +0 -0
  134. package/components/tryghost-members-events-service-5.14.1.tgz +0 -0
  135. package/components/tryghost-members-importer-5.14.1.tgz +0 -0
  136. package/components/tryghost-members-offers-5.14.1.tgz +0 -0
  137. package/components/tryghost-members-stripe-service-5.14.1.tgz +0 -0
  138. package/components/tryghost-minifier-5.14.1.tgz +0 -0
  139. package/components/tryghost-mw-api-version-mismatch-5.14.1.tgz +0 -0
  140. package/components/tryghost-mw-cache-control-5.14.1.tgz +0 -0
  141. package/components/tryghost-mw-session-from-token-5.14.1.tgz +0 -0
  142. package/components/tryghost-mw-update-user-last-seen-5.14.1.tgz +0 -0
  143. package/components/tryghost-mw-vhost-5.14.1.tgz +0 -0
  144. package/components/tryghost-security-5.14.1.tgz +0 -0
  145. package/components/tryghost-staff-service-5.14.1.tgz +0 -0
  146. package/components/tryghost-version-notifications-data-service-5.14.1.tgz +0 -0
  147. package/core/built/admin/assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css +0 -1
  148. package/core/built/admin/assets/ghost-dark-13b669d50f494edf24d832b32ece2177.css +0 -1
  149. package/core/server/services/permissions/public.js +0 -76
@@ -46,11 +46,13 @@ module.exports = class ExploreService {
46
46
  }
47
47
  };
48
48
 
49
- const mostRecentlyPublishedPost = await this.PostsService.getMostRecentlyPublishedPost();
50
- exploreProperties.most_recently_published_at = mostRecentlyPublishedPost?.get('published_at') || null;
49
+ const mostRecentlyPublishedPost = await this.PostsService.stats.getMostRecentlyPublishedPostDate();
50
+ const totalPostsPublished = await this.PostsService.stats.getTotalPostsPublished();
51
+ exploreProperties.most_recently_published_at = mostRecentlyPublishedPost ?? null;
52
+ exploreProperties.total_posts_published = totalPostsPublished ?? null;
51
53
 
52
54
  const owner = await this.UserModel.findOne({role: 'Owner', status: 'all'});
53
- exploreProperties.owner_email = owner?.get('email') || null;
55
+ exploreProperties.owner_email = owner?.get('email') ?? null;
54
56
 
55
57
  return exploreProperties;
56
58
  }
@@ -0,0 +1,25 @@
1
+ class LinkTrackingServiceWrapper {
2
+ async init() {
3
+ if (this.service) {
4
+ // Already done
5
+ return;
6
+ }
7
+
8
+ // Wire up all the dependencies
9
+ const LinkTrackingService = require('@tryghost/link-tracking');
10
+
11
+ // Expose the service
12
+ this.service = new LinkTrackingService({
13
+ linkClickRepository: {
14
+ async save(linkClick) {
15
+ // eslint-disable-next-line no-console
16
+ console.log('Saving link click', linkClick);
17
+ }
18
+ }
19
+ });
20
+
21
+ await this.service.init();
22
+ }
23
+ }
24
+
25
+ module.exports = new LinkTrackingServiceWrapper();
@@ -0,0 +1,33 @@
1
+ const urlUtils = require('../../../shared/url-utils');
2
+
3
+ class LinkRedirectsServiceWrapper {
4
+ async init() {
5
+ if (this.service) {
6
+ // Already done
7
+ return;
8
+ }
9
+
10
+ // Wire up all the dependencies
11
+ const {LinkRedirectsService} = require('@tryghost/link-redirects');
12
+
13
+ const store = [];
14
+ // Expose the service
15
+ this.service = new LinkRedirectsService({
16
+ linkRedirectRepository: {
17
+ async save(linkRedirect) {
18
+ store.push(linkRedirect);
19
+ },
20
+ async getByURL(url) {
21
+ return store.find((link) => {
22
+ return link.from.pathname === url.pathname;
23
+ });
24
+ }
25
+ },
26
+ config: {
27
+ baseURL: new URL(urlUtils.getSiteUrl())
28
+ }
29
+ });
30
+ }
31
+ }
32
+
33
+ module.exports = new LinkRedirectsServiceWrapper();
@@ -0,0 +1,24 @@
1
+ class LinkReplacementServiceWrapper {
2
+ init() {
3
+ if (this.service) {
4
+ // Already done
5
+ return;
6
+ }
7
+
8
+ // Wire up all the dependencies
9
+ const LinkReplacementService = require('@tryghost/link-replacement');
10
+ const urlUtils = require('../../../shared/url-utils');
11
+ const settingsCache = require('../../../shared/settings-cache');
12
+
13
+ // Expose the service
14
+ this.service = new LinkReplacementService({
15
+ linkRedirectService: require('../link-redirection').service,
16
+ linkClickTrackingService: require('../link-click-tracking').service,
17
+ attributionService: require('../member-attribution').service,
18
+ urlUtils,
19
+ settingsCache
20
+ });
21
+ }
22
+ }
23
+
24
+ module.exports = new LinkReplacementServiceWrapper();
@@ -27,6 +27,7 @@ class EmailPreview {
27
27
  emailContent = postEmailSerializer.renderEmailForSegment(emailContent, memberSegment);
28
28
  }
29
29
 
30
+ // Do fake replacements, just like a normal email, but use fallbacks and empty values
30
31
  const replacements = postEmailSerializer.parseReplacements(emailContent);
31
32
 
32
33
  replacements.forEach((replacement) => {
@@ -36,6 +37,12 @@ class EmailPreview {
36
37
  );
37
38
  });
38
39
 
40
+ // Replace unsubscribe URL (%recipient.unsubscribe_url% replacement)
41
+ // We should do this only here because replacements should happen at the very end only, just like when an actual email would be send
42
+ const previewUnsubscribeUrl = postEmailSerializer.createUnsubscribeUrl(null);
43
+ emailContent.html = emailContent.html.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
44
+ emailContent.plaintext = emailContent.plaintext.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
45
+
39
46
  return {
40
47
  subject: emailContent.subject,
41
48
  html: emailContent.html,
@@ -85,7 +85,7 @@ const getEmailData = async (postModel, options) => {
85
85
  * @param {ValidMemberSegment} [memberSegment]
86
86
  */
87
87
  const sendTestEmail = async (postModel, toEmails, memberSegment) => {
88
- let emailData = await getEmailData(postModel);
88
+ let emailData = await getEmailData(postModel, {isTestEmail: true});
89
89
  emailData.subject = `[Test] ${emailData.subject}`;
90
90
 
91
91
  // fetch any matching members so that replacements use expected values
@@ -14,8 +14,9 @@ const {isUnsplashImage, isLocalContentImage} = require('@tryghost/kg-default-car
14
14
  const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@tryghost/color-utils');
15
15
  const logging = require('@tryghost/logging');
16
16
  const urlService = require('../../services/url');
17
+ const linkReplacement = require('../link-replacement');
17
18
 
18
- const ALLOWED_REPLACEMENTS = ['first_name'];
19
+ const ALLOWED_REPLACEMENTS = ['first_name', 'uuid'];
19
20
 
20
21
  const PostEmailSerializer = {
21
22
 
@@ -243,7 +244,7 @@ const PostEmailSerializer = {
243
244
  return templateSettings;
244
245
  },
245
246
 
246
- async serialize(postModel, newsletter, options = {isBrowserPreview: false}) {
247
+ async serialize(postModel, newsletter, options = {isBrowserPreview: false, isTestEmail: false}) {
247
248
  const post = await this.serializePostModel(postModel);
248
249
 
249
250
  const timezone = settingsCache.get('timezone');
@@ -276,7 +277,7 @@ const PostEmailSerializer = {
276
277
  // perform any email specific adjustments to the mobiledoc->HTML render output
277
278
  // body wrapper is required so we can get proper top-level selections
278
279
  const cheerio = require('cheerio');
279
- let _cheerio = cheerio.load(`<body>${post.html}</body>`);
280
+ const _cheerio = cheerio.load(`<body>${post.html}</body>`);
280
281
  // remove leading/trailing HRs
281
282
  _cheerio(`
282
283
  body > hr:first-child,
@@ -284,8 +285,10 @@ const PostEmailSerializer = {
284
285
  body > div:first-child > hr:first-child,
285
286
  body > div:last-child > hr:last-child
286
287
  `).remove();
287
- post.html = _cheerio('body').html();
288
+ post.html = _cheerio('body').html(); // () (added this comment because of a bug in the syntax highlighter in VSCode)
288
289
 
290
+ // Note: we don't need to do link replacements on the plaintext here
291
+ // because the plaintext will get recalculated on the updated post html (which already includes link replacements) in renderEmailForSegment
289
292
  post.plaintext = htmlToPlaintext.email(post.html);
290
293
 
291
294
  // Outlook will render feature images at full-size breaking the layout.
@@ -325,24 +328,49 @@ const PostEmailSerializer = {
325
328
 
326
329
  let htmlTemplate = render({post, site: this.getSite(), templateSettings, newsletter: newsletter.toJSON()});
327
330
 
328
- if (options.isBrowserPreview) {
329
- const previewUnsubscribeUrl = this.createUnsubscribeUrl(null);
330
- htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
331
+ // The plaintext version that is returned here is actually never really used for sending because we'll use htmlToPlaintext again later
332
+ let result = {
333
+ html: this.formatHtmlForEmail(htmlTemplate),
334
+ plaintext: post.plaintext
335
+ };
336
+
337
+ /**
338
+ * If a part of the email is members-only and the post is paid-only, add a paywall:
339
+ * - Just before sending the email, we'll hide the paywall or paid content depending on the member segment it is sent to.
340
+ * - We already need to do URL-replacement on the HTML here
341
+ * - Link replacement cannot happen later because renderEmailForSegment is called multiple times for a single email (which would result in duplicate redirects)
342
+ */
343
+ const isPaidPost = post.visibility === 'paid' || post.visibility === 'tiers';
344
+
345
+ const paywallIndex = (result.html || '').indexOf('<!--members-only-->');
346
+ if (paywallIndex !== -1 && isPaidPost) {
347
+ const postContentEndIdx = result.html.indexOf('<!-- POST CONTENT END -->');
348
+
349
+ if (postContentEndIdx !== -1) {
350
+ const paywallHTML = '<!-- PAYWALL -->' + this.renderPaywallCTA(post);
351
+
352
+ // Append it just before the end of the post content
353
+ result.html = result.html.slice(0, postContentEndIdx) + paywallHTML + result.html.slice(postContentEndIdx);
354
+ }
355
+ }
356
+
357
+ // Now replace the links in the HTML version
358
+ if (labs.isSet('emailClicks')) {
359
+ if ((!options.isBrowserPreview && !options.isTestEmail) || process.env.NODE_ENV === 'development') {
360
+ result.html = await linkReplacement.service.replaceLinks(result.html, newsletter, postModel);
361
+ }
331
362
  }
332
363
 
333
364
  // Clean up any unknown replacements strings to get our final content
334
- const {html, plaintext} = this.normalizeReplacementStrings({
335
- html: this.formatHtmlForEmail(htmlTemplate),
336
- plaintext: post.plaintext
337
- });
365
+ const {html, plaintext} = this.normalizeReplacementStrings(result);
338
366
  const data = {
339
367
  subject: post.email_subject || post.title,
340
368
  html,
341
369
  plaintext
342
370
  };
343
- if (labs.isSet('newsletterPaywall')) {
344
- data.post = post;
345
- }
371
+
372
+ // Add post for checking access in renderEmailForSegment (only for previews)
373
+ data.post = post;
346
374
  return data;
347
375
  },
348
376
 
@@ -392,25 +420,45 @@ const PostEmailSerializer = {
392
420
 
393
421
  const result = {...email};
394
422
 
395
- /** Checks and hides content for newsletter behind paywall card
396
- * based on member's status and post access
397
- * Adds CTA in case content is hidden.
398
- */
399
- if (labs.isSet('newsletterPaywall')) {
400
- const paywallIndex = (result.html || '').indexOf('<!--members-only-->');
401
- if (paywallIndex !== -1 && memberSegment && result.post) {
423
+ // Note about link tracking:
424
+ // Don't add new HTML in here, but add it in the serialize method and surround it with the required HTML comments or attributes
425
+ // This is because we can't replace links at this point (this is executed multiple times, once per batch and we don't want to generate duplicate links for the same email)
426
+
427
+ // Remove the paywall or members-only content based on the current member segment
428
+ const startMembersOnlyContent = (result.html || '').indexOf('<!--members-only-->');
429
+ const startPaywall = result.html.indexOf('<!-- PAYWALL -->');
430
+ let endPost = result.html.indexOf('<!-- POST CONTENT END -->');
431
+
432
+ if (endPost === -1) {
433
+ // Default to the end of the HTML (shouldn't happen, but just in case if we have members-only content that should get removed)
434
+ endPost = result.html.length;
435
+ }
436
+
437
+ // We support the cases where there is no <!--members-only--> but there is a paywall (in case of bugs)
438
+ // We also support the case where there is no <!-- PAYWALL --> but there is a <!--members-only--> (in case of bugs)
439
+ if (startMembersOnlyContent !== -1 || startPaywall !== -1) {
440
+ // By default remove the paywall if no memberSegment is passed
441
+ let memberHasAccess = true;
442
+
443
+ if (memberSegment && result.post) {
402
444
  let statusFilter = memberSegment === 'status:free' ? {status: 'free'} : {status: 'paid'};
403
445
  const postVisiblity = result.post.visibility;
404
446
 
405
447
  // For newsletter paywall, specific tiers visibility is considered on par to paid tiers
406
448
  result.post.visibility = postVisiblity === 'tiers' ? 'paid' : postVisiblity;
407
449
 
408
- const memberHasAccess = membersService.contentGating.checkPostAccess(result.post, statusFilter);
450
+ memberHasAccess = membersService.contentGating.checkPostAccess(result.post, statusFilter);
451
+ }
409
452
 
410
- if (!memberHasAccess) {
411
- const postContentEndIdx = result.html.search(/[\s\n\r]+?<!-- POST CONTENT END -->/);
412
- result.html = result.html.slice(0, paywallIndex) + this.renderPaywallCTA(result.post) + result.html.slice(postContentEndIdx);
413
- result.plaintext = htmlToPlaintext.excerpt(result.html);
453
+ if (!memberHasAccess) {
454
+ if (startMembersOnlyContent !== -1) {
455
+ // Remove the members-only content, but keep the paywall (if there is a paywall)
456
+ result.html = result.html.slice(0, startMembersOnlyContent) + result.html.slice(startPaywall === -1 ? endPost : startPaywall);
457
+ }
458
+ } else {
459
+ if (startPaywall !== -1) {
460
+ // Remove the paywall
461
+ result.html = result.html.slice(0, startPaywall) + result.html.slice(endPost);
414
462
  }
415
463
  }
416
464
  }
@@ -427,7 +475,7 @@ const PostEmailSerializer = {
427
475
  });
428
476
 
429
477
  result.html = this.formatHtmlForEmail($.html());
430
- result.plaintext = htmlToPlaintext.email(result.html);
478
+ result.plaintext = htmlToPlaintext.email(result.html);
431
479
  delete result.post;
432
480
 
433
481
  return result;
@@ -26,7 +26,7 @@ const sanitizeKeys = (obj, keys) => {
26
26
  module.exports = ({post, site, newsletter, templateSettings}) => {
27
27
  const date = new Date();
28
28
  const hasFeatureImageCaption = templateSettings.showFeatureImage && post.feature_image && post.feature_image_caption;
29
- const cleanPost = sanitizeKeys(post, ['url', 'published_at', 'title', 'excerpt', 'authors', 'feature_image', 'feature_image_alt', 'feature_image_caption']);
29
+ const cleanPost = sanitizeKeys(post, ['url', 'published_at', 'title', 'excerpt', 'authors', 'feature_image', 'feature_image_alt']);
30
30
  const cleanSite = sanitizeKeys(site, ['title']);
31
31
  const cleanNewsletter = sanitizeKeys(newsletter, ['name']);
32
32
 
@@ -13,7 +13,6 @@ 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');
17
16
  const newslettersService = require('../newsletters');
18
17
  const memberAttributionService = require('../member-attribution');
19
18
 
@@ -198,7 +197,6 @@ function createApiInstance(config) {
198
197
  },
199
198
  stripeAPIService: stripeService.api,
200
199
  offersAPI: offersService.api,
201
- staffService: staffService.api,
202
200
  labsService: labsService,
203
201
  newslettersService: newslettersService,
204
202
  memberAttributionService: memberAttributionService.service
@@ -19,6 +19,5 @@ module.exports = {
19
19
  init: init,
20
20
  canThis: require('./can-this'),
21
21
  // @TODO: Make it so that we don't need to export these
22
- parseContext: require('./parse-context'),
23
- applyPublicRules: require('./public')
22
+ parseContext: require('./parse-context')
24
23
  };
@@ -8,11 +8,12 @@ const messages = {
8
8
  };
9
9
 
10
10
  class PostsService {
11
- constructor({mega, urlUtils, models, isSet}) {
11
+ constructor({mega, urlUtils, models, isSet, stats}) {
12
12
  this.mega = mega;
13
13
  this.urlUtils = urlUtils;
14
14
  this.models = models;
15
15
  this.isSet = isSet;
16
+ this.stats = stats;
16
17
  }
17
18
 
18
19
  async editPost(frame) {
@@ -76,20 +77,6 @@ class PostsService {
76
77
  }
77
78
  }
78
79
 
79
- /**
80
- * Returns the most recently `published_at` post that was published or sent
81
- * via email
82
- */
83
- async getMostRecentlyPublishedPost() {
84
- const recentlyPublishedPost = await this.models.Post.findPage({
85
- status: ['published', 'sent'],
86
- order: 'published_at DESC',
87
- limit: 1
88
- });
89
-
90
- return recentlyPublishedPost?.data[0];
91
- }
92
-
93
80
  /**
94
81
  * Calculates if the email should be tried to be sent out
95
82
  * @private
@@ -135,12 +122,16 @@ const getPostServiceInstance = () => {
135
122
  const {mega} = require('../mega');
136
123
  const labs = require('../../../shared/labs');
137
124
  const models = require('../../models');
125
+ const PostStats = require('./stats/post-stats');
126
+
127
+ const postStats = new PostStats();
138
128
 
139
129
  return new PostsService({
140
130
  mega: mega,
141
131
  urlUtils: urlUtils,
142
132
  models: models,
143
- isSet: labs.isSet.bind(labs)
133
+ isSet: labs.isSet.bind(labs),
134
+ stats: postStats
144
135
  });
145
136
  };
146
137
 
@@ -0,0 +1,35 @@
1
+ const db = require('../../../data/db');
2
+ class PostStats {
3
+ #db;
4
+
5
+ constructor() {
6
+ this.#db = db;
7
+ }
8
+
9
+ /**
10
+ * Returns the most recently `published_at` post that was published or sent
11
+ * via email
12
+ */
13
+ async getMostRecentlyPublishedPostDate() {
14
+ const result = await this.#db.knex.select('published_at')
15
+ .from('posts')
16
+ .whereIn('status', ['sent', 'published'])
17
+ .orderBy('published_at', 'desc')
18
+ .limit(1);
19
+
20
+ return result?.[0]?.published_at ? new Date(result?.[0]?.published_at) : null;
21
+ }
22
+
23
+ /**
24
+ * Fetches count of all published posts
25
+ */
26
+ async getTotalPostsPublished() {
27
+ const [result] = await this.#db.knex('posts')
28
+ .whereIn('status', ['sent', 'published'])
29
+ .count('id', {as: 'total'});
30
+
31
+ return result.total;
32
+ }
33
+ }
34
+
35
+ module.exports = PostStats;
@@ -1,5 +1,11 @@
1
+ const DomainEvents = require('@tryghost/domain-events');
1
2
  class StaffServiceWrapper {
2
3
  init() {
4
+ if (this.api) {
5
+ // Prevent creating duplicate DomainEvents subscribers
6
+ return;
7
+ }
8
+
3
9
  const StaffService = require('@tryghost/staff-service');
4
10
 
5
11
  const logging = require('@tryghost/logging');
@@ -16,8 +22,11 @@ class StaffServiceWrapper {
16
22
  mailer,
17
23
  settingsHelpers,
18
24
  settingsCache,
19
- urlUtils
25
+ urlUtils,
26
+ DomainEvents
20
27
  });
28
+
29
+ this.api.subscribeEvents();
21
30
  }
22
31
  }
23
32
 
@@ -12,6 +12,7 @@ module.exports = [
12
12
  exclude: [
13
13
  'title',
14
14
  'mobiledoc',
15
+ 'lexical',
15
16
  'html',
16
17
  'plaintext',
17
18
  // @TODO: https://github.com/TryGhost/Ghost/issues/10335
@@ -55,6 +56,7 @@ module.exports = [
55
56
  exclude: [
56
57
  'title',
57
58
  'mobiledoc',
59
+ 'lexical',
58
60
  'html',
59
61
  'plaintext',
60
62
  // @TODO: https://github.com/TryGhost/Ghost/issues/10335
@@ -153,8 +153,8 @@
153
153
  "version": "0.10.1"
154
154
  },
155
155
  "editor": {
156
- "url": "https://unpkg.com/@tryghost/koenig-react/dist/umd/koenig-react.min.js",
157
- "lexicalUrl": "https://unpkg.com/@tryghost/koenig-lexical-experiment/dist/koenig-lexical.umd.js"
156
+ "url": "https://unpkg.com/@tryghost/koenig-lexical@~{version}/dist/koenig-lexical.umd.js",
157
+ "version": "0.0"
158
158
  },
159
159
  "tenor": {
160
160
  "googleApiKey": null,
@@ -30,7 +30,7 @@
30
30
  "contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon", "image/webp"]
31
31
  },
32
32
  "media": {
33
- "extensions": [".mp4",".webm", ".ogv", ".mp3", ".wav", ".ogg"],
33
+ "extensions": [".mp4",".webm", ".ogv", ".mp3", ".wav", ".ogg", ".m4a"],
34
34
  "contentTypes": [
35
35
  "video/mp4",
36
36
  "video/webm",
@@ -40,7 +40,8 @@
40
40
  "audio/wave",
41
41
  "audio/wav",
42
42
  "audio/x-wav",
43
- "audio/ogg"
43
+ "audio/ogg",
44
+ "audio/x-m4a"
44
45
  ]
45
46
  },
46
47
  "thumbnails": {
@@ -15,7 +15,6 @@ 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
- 'auditLog',
19
18
  'newsletterPaywall',
20
19
  'freeTrial',
21
20
  'compExpiring',
@@ -32,7 +31,10 @@ const BETA_FEATURES = [
32
31
 
33
32
  const ALPHA_FEATURES = [
34
33
  'urlCache',
35
- 'beforeAfterCard'
34
+ 'beforeAfterCard',
35
+ 'emailClicks',
36
+ 'sourceAttribution',
37
+ 'lexicalEditor'
36
38
  ];
37
39
 
38
40
  module.exports.GA_KEYS = [...GA_FEATURES];