ghost 5.110.4 → 5.111.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 (191) hide show
  1. package/components/tryghost-activitypub-5.111.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-memory-ttl-5.111.0.tgz +0 -0
  3. package/components/tryghost-adapter-cache-redis-5.111.0.tgz +0 -0
  4. package/components/{tryghost-adapter-manager-5.110.4.tgz → tryghost-adapter-manager-5.111.0.tgz} +0 -0
  5. package/components/tryghost-announcement-bar-settings-5.111.0.tgz +0 -0
  6. package/components/{tryghost-api-framework-5.110.4.tgz → tryghost-api-framework-5.111.0.tgz} +0 -0
  7. package/components/tryghost-api-version-compatibility-service-5.111.0.tgz +0 -0
  8. package/components/{tryghost-audience-feedback-5.110.4.tgz → tryghost-audience-feedback-5.111.0.tgz} +0 -0
  9. package/components/tryghost-bookshelf-repository-5.111.0.tgz +0 -0
  10. package/components/tryghost-bootstrap-socket-5.111.0.tgz +0 -0
  11. package/components/tryghost-captcha-service-5.111.0.tgz +0 -0
  12. package/components/tryghost-constants-5.111.0.tgz +0 -0
  13. package/components/tryghost-custom-fonts-5.111.0.tgz +0 -0
  14. package/components/{tryghost-custom-theme-settings-service-5.110.4.tgz → tryghost-custom-theme-settings-service-5.111.0.tgz} +0 -0
  15. package/components/{tryghost-data-generator-5.110.4.tgz → tryghost-data-generator-5.111.0.tgz} +0 -0
  16. package/components/tryghost-domain-events-5.111.0.tgz +0 -0
  17. package/components/tryghost-donations-5.111.0.tgz +0 -0
  18. package/components/tryghost-email-addresses-5.111.0.tgz +0 -0
  19. package/components/tryghost-email-analytics-provider-mailgun-5.111.0.tgz +0 -0
  20. package/components/{tryghost-email-analytics-service-5.110.4.tgz → tryghost-email-analytics-service-5.111.0.tgz} +0 -0
  21. package/components/tryghost-email-content-generator-5.111.0.tgz +0 -0
  22. package/components/tryghost-email-events-5.111.0.tgz +0 -0
  23. package/components/tryghost-email-service-5.111.0.tgz +0 -0
  24. package/components/tryghost-email-suppression-list-5.111.0.tgz +0 -0
  25. package/components/{tryghost-express-dynamic-redirects-5.110.4.tgz → tryghost-express-dynamic-redirects-5.111.0.tgz} +0 -0
  26. package/components/{tryghost-external-media-inliner-5.110.4.tgz → tryghost-external-media-inliner-5.111.0.tgz} +0 -0
  27. package/components/{tryghost-extract-api-key-5.110.4.tgz → tryghost-extract-api-key-5.111.0.tgz} +0 -0
  28. package/components/tryghost-ghost-5.111.0.tgz +0 -0
  29. package/components/{tryghost-html-to-plaintext-5.110.4.tgz → tryghost-html-to-plaintext-5.111.0.tgz} +0 -0
  30. package/components/tryghost-i18n-5.111.0.tgz +0 -0
  31. package/components/tryghost-identity-token-service-5.111.0.tgz +0 -0
  32. package/components/tryghost-importer-handler-content-files-5.111.0.tgz +0 -0
  33. package/components/{tryghost-importer-revue-5.110.4.tgz → tryghost-importer-revue-5.111.0.tgz} +0 -0
  34. package/components/tryghost-in-memory-repository-5.111.0.tgz +0 -0
  35. package/components/{tryghost-job-manager-5.110.4.tgz → tryghost-job-manager-5.111.0.tgz} +0 -0
  36. package/components/tryghost-link-redirects-5.111.0.tgz +0 -0
  37. package/components/tryghost-link-replacer-5.111.0.tgz +0 -0
  38. package/components/{tryghost-magic-link-5.110.4.tgz → tryghost-magic-link-5.111.0.tgz} +0 -0
  39. package/components/tryghost-mail-events-5.111.0.tgz +0 -0
  40. package/components/tryghost-mailgun-client-5.111.0.tgz +0 -0
  41. package/components/{tryghost-member-attribution-5.110.4.tgz → tryghost-member-attribution-5.111.0.tgz} +0 -0
  42. package/components/tryghost-member-events-5.111.0.tgz +0 -0
  43. package/components/{tryghost-members-api-5.110.4.tgz → tryghost-members-api-5.111.0.tgz} +0 -0
  44. package/components/tryghost-members-csv-5.111.0.tgz +0 -0
  45. package/components/{tryghost-members-importer-5.110.4.tgz → tryghost-members-importer-5.111.0.tgz} +0 -0
  46. package/components/{tryghost-members-offers-5.110.4.tgz → tryghost-members-offers-5.111.0.tgz} +0 -0
  47. package/components/tryghost-members-payments-5.111.0.tgz +0 -0
  48. package/components/{tryghost-members-ssr-5.110.4.tgz → tryghost-members-ssr-5.111.0.tgz} +0 -0
  49. package/components/{tryghost-members-stripe-service-5.110.4.tgz → tryghost-members-stripe-service-5.111.0.tgz} +0 -0
  50. package/components/{tryghost-milestones-5.110.4.tgz → tryghost-milestones-5.111.0.tgz} +0 -0
  51. package/components/tryghost-minifier-5.111.0.tgz +0 -0
  52. package/components/tryghost-mw-api-version-mismatch-5.111.0.tgz +0 -0
  53. package/components/tryghost-mw-cache-control-5.111.0.tgz +0 -0
  54. package/components/tryghost-mw-error-handler-5.111.0.tgz +0 -0
  55. package/components/{tryghost-mw-session-from-token-5.110.4.tgz → tryghost-mw-session-from-token-5.111.0.tgz} +0 -0
  56. package/components/{tryghost-mw-update-user-last-seen-5.110.4.tgz → tryghost-mw-update-user-last-seen-5.111.0.tgz} +0 -0
  57. package/components/{tryghost-mw-version-match-5.110.4.tgz → tryghost-mw-version-match-5.111.0.tgz} +0 -0
  58. package/components/tryghost-mw-vhost-5.111.0.tgz +0 -0
  59. package/components/tryghost-package-json-5.111.0.tgz +0 -0
  60. package/components/tryghost-post-events-5.111.0.tgz +0 -0
  61. package/components/tryghost-post-revisions-5.111.0.tgz +0 -0
  62. package/components/{tryghost-posts-service-5.110.4.tgz → tryghost-posts-service-5.111.0.tgz} +0 -0
  63. package/components/tryghost-prometheus-metrics-5.111.0.tgz +0 -0
  64. package/components/tryghost-recommendations-5.111.0.tgz +0 -0
  65. package/components/tryghost-referrers-5.111.0.tgz +0 -0
  66. package/components/{tryghost-security-5.110.4.tgz → tryghost-security-5.111.0.tgz} +0 -0
  67. package/components/{tryghost-session-service-5.110.4.tgz → tryghost-session-service-5.111.0.tgz} +0 -0
  68. package/components/tryghost-settings-path-manager-5.111.0.tgz +0 -0
  69. package/components/{tryghost-slack-notifications-5.110.4.tgz → tryghost-slack-notifications-5.111.0.tgz} +0 -0
  70. package/components/tryghost-stats-service-5.111.0.tgz +0 -0
  71. package/components/{tryghost-tiers-5.110.4.tgz → tryghost-tiers-5.111.0.tgz} +0 -0
  72. package/components/tryghost-version-notifications-data-service-5.111.0.tgz +0 -0
  73. package/components/tryghost-webmentions-5.111.0.tgz +0 -0
  74. package/core/boot.js +3 -0
  75. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +11401 -9442
  76. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
  77. package/core/built/admin/assets/admin-x-demo/{index-82e381fb.mjs → index-0040480a.mjs} +3252 -2891
  78. package/core/built/admin/assets/admin-x-demo/{modals-b20a9ede.mjs → modals-fb35c86c.mjs} +2 -2
  79. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ea62c29b.mjs → CodeEditorView-1298238e.mjs} +624 -618
  80. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  81. package/core/built/admin/assets/admin-x-settings/{index-4b25c788.mjs → index-0f51ccb5.mjs} +2 -2
  82. package/core/built/admin/assets/admin-x-settings/{index-af8cf9cf.mjs → index-2707471f.mjs} +6759 -6372
  83. package/core/built/admin/assets/admin-x-settings/{modals-cb2dc7b7.mjs → modals-f5983704.mjs} +7887 -7668
  84. package/core/built/admin/assets/{chunk.524.3096e68df5b51dacf872.js → chunk.524.405c43b2cb20553b51d9.js} +6 -6
  85. package/core/built/admin/assets/{chunk.582.e225422f90639ff30544.js → chunk.582.eb4b096f29c97c9d6a64.js} +9 -9
  86. package/core/built/admin/assets/{ghost-98d002d50a5e01d2100b2c387a849249.js → ghost-87cffc153ec73d217c1ae9f9207ea5e1.js} +36 -37
  87. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  88. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +16898 -16284
  89. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +224 -197
  90. package/core/built/admin/assets/posts/posts.js +24137 -24156
  91. package/core/built/admin/index.html +3 -3
  92. package/core/demo.js +6 -0
  93. package/core/demo.ts +3 -0
  94. package/core/frontend/services/sitemap/SiteMapManager.js +1 -1
  95. package/core/frontend/src/cards/css/cta.css +13 -5
  96. package/core/frontend/src/cards/css/video.css +1 -0
  97. package/core/server/api/endpoints/settings-public.js +3 -2
  98. package/core/server/api/endpoints/utils/serializers/input/settings.js +2 -1
  99. package/core/server/data/migrations/versions/5.111/2025-03-05-16-36-39-add-captcha-setting.js +8 -0
  100. package/core/server/data/schema/default-settings/default-settings.json +8 -0
  101. package/core/server/services/link-tracking/ClickEvent.js +25 -0
  102. package/core/server/services/link-tracking/FullPostLink.js +36 -0
  103. package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
  104. package/core/server/services/link-tracking/LinkClickTrackingService.js +237 -0
  105. package/core/server/services/link-tracking/PostLink.js +29 -0
  106. package/core/server/services/link-tracking/PostLinkRepository.js +2 -2
  107. package/core/server/services/link-tracking/index.js +1 -1
  108. package/core/server/services/members-events/EventStorage.js +61 -0
  109. package/core/server/services/members-events/LastSeenAtCache.js +96 -0
  110. package/core/server/services/members-events/LastSeenAtUpdater.js +192 -0
  111. package/core/server/services/members-events/index.js +3 -1
  112. package/core/server/services/mentions-email-report/MentionEmailReportJob.js +117 -0
  113. package/core/server/services/mentions-email-report/service.js +3 -3
  114. package/core/server/services/staff/StaffService.js +179 -0
  115. package/core/server/services/staff/StaffServiceEmails.js +527 -0
  116. package/core/server/services/staff/email-templates/donation.hbs +119 -0
  117. package/core/server/services/staff/email-templates/donation.txt.js +15 -0
  118. package/core/server/services/staff/email-templates/mention-report.hbs +136 -0
  119. package/core/server/services/staff/email-templates/mention-report.txt.js +19 -0
  120. package/core/server/services/staff/email-templates/new-free-signup.hbs +118 -0
  121. package/core/server/services/staff/email-templates/new-free-signup.txt.js +13 -0
  122. package/core/server/services/staff/email-templates/new-milestone-received.hbs +142 -0
  123. package/core/server/services/staff/email-templates/new-milestone-received.txt.js +13 -0
  124. package/core/server/services/staff/email-templates/new-paid-cancellation.hbs +125 -0
  125. package/core/server/services/staff/email-templates/new-paid-cancellation.txt.js +13 -0
  126. package/core/server/services/staff/email-templates/new-paid-started.hbs +124 -0
  127. package/core/server/services/staff/email-templates/new-paid-started.txt.js +13 -0
  128. package/core/server/services/staff/email-templates/partials/preview.hbs +6 -0
  129. package/core/server/services/staff/email-templates/partials/styles.hbs +114 -0
  130. package/core/server/services/staff/email-templates/recommendation-received.hbs +154 -0
  131. package/core/server/services/staff/email-templates/recommendation-received.txt.js +13 -0
  132. package/core/server/services/staff/index.js +1 -1
  133. package/core/server/services/staff/milestone-email-config.js +207 -0
  134. package/core/server/services/url/Resources.js +1 -1
  135. package/core/shared/config/defaults.json +2 -1
  136. package/core/shared/events/URLResourceUpdatedEvent.js +33 -0
  137. package/core/shared/settings-cache/public.js +1 -0
  138. package/package.json +157 -157
  139. package/tsconfig.json +105 -0
  140. package/tsconfig.tsbuildinfo +1 -0
  141. package/yarn.lock +342 -89
  142. package/components/tryghost-activitypub-5.110.4.tgz +0 -0
  143. package/components/tryghost-adapter-cache-memory-ttl-5.110.4.tgz +0 -0
  144. package/components/tryghost-adapter-cache-redis-5.110.4.tgz +0 -0
  145. package/components/tryghost-announcement-bar-settings-5.110.4.tgz +0 -0
  146. package/components/tryghost-api-version-compatibility-service-5.110.4.tgz +0 -0
  147. package/components/tryghost-bookshelf-repository-5.110.4.tgz +0 -0
  148. package/components/tryghost-bootstrap-socket-5.110.4.tgz +0 -0
  149. package/components/tryghost-captcha-service-5.110.4.tgz +0 -0
  150. package/components/tryghost-constants-5.110.4.tgz +0 -0
  151. package/components/tryghost-custom-fonts-5.110.4.tgz +0 -0
  152. package/components/tryghost-domain-events-5.110.4.tgz +0 -0
  153. package/components/tryghost-donations-5.110.4.tgz +0 -0
  154. package/components/tryghost-dynamic-routing-events-5.110.4.tgz +0 -0
  155. package/components/tryghost-email-addresses-5.110.4.tgz +0 -0
  156. package/components/tryghost-email-analytics-provider-mailgun-5.110.4.tgz +0 -0
  157. package/components/tryghost-email-content-generator-5.110.4.tgz +0 -0
  158. package/components/tryghost-email-events-5.110.4.tgz +0 -0
  159. package/components/tryghost-email-service-5.110.4.tgz +0 -0
  160. package/components/tryghost-email-suppression-list-5.110.4.tgz +0 -0
  161. package/components/tryghost-ghost-5.110.4.tgz +0 -0
  162. package/components/tryghost-i18n-5.110.4.tgz +0 -0
  163. package/components/tryghost-identity-token-service-5.110.4.tgz +0 -0
  164. package/components/tryghost-importer-handler-content-files-5.110.4.tgz +0 -0
  165. package/components/tryghost-in-memory-repository-5.110.4.tgz +0 -0
  166. package/components/tryghost-link-redirects-5.110.4.tgz +0 -0
  167. package/components/tryghost-link-replacer-5.110.4.tgz +0 -0
  168. package/components/tryghost-link-tracking-5.110.4.tgz +0 -0
  169. package/components/tryghost-mail-events-5.110.4.tgz +0 -0
  170. package/components/tryghost-mailgun-client-5.110.4.tgz +0 -0
  171. package/components/tryghost-member-events-5.110.4.tgz +0 -0
  172. package/components/tryghost-members-csv-5.110.4.tgz +0 -0
  173. package/components/tryghost-members-events-service-5.110.4.tgz +0 -0
  174. package/components/tryghost-members-payments-5.110.4.tgz +0 -0
  175. package/components/tryghost-mentions-email-report-5.110.4.tgz +0 -0
  176. package/components/tryghost-minifier-5.110.4.tgz +0 -0
  177. package/components/tryghost-mw-api-version-mismatch-5.110.4.tgz +0 -0
  178. package/components/tryghost-mw-cache-control-5.110.4.tgz +0 -0
  179. package/components/tryghost-mw-error-handler-5.110.4.tgz +0 -0
  180. package/components/tryghost-mw-vhost-5.110.4.tgz +0 -0
  181. package/components/tryghost-package-json-5.110.4.tgz +0 -0
  182. package/components/tryghost-post-events-5.110.4.tgz +0 -0
  183. package/components/tryghost-post-revisions-5.110.4.tgz +0 -0
  184. package/components/tryghost-prometheus-metrics-5.110.4.tgz +0 -0
  185. package/components/tryghost-recommendations-5.110.4.tgz +0 -0
  186. package/components/tryghost-referrers-5.110.4.tgz +0 -0
  187. package/components/tryghost-settings-path-manager-5.110.4.tgz +0 -0
  188. package/components/tryghost-staff-service-5.110.4.tgz +0 -0
  189. package/components/tryghost-stats-service-5.110.4.tgz +0 -0
  190. package/components/tryghost-version-notifications-data-service-5.110.4.tgz +0 -0
  191. package/components/tryghost-webmentions-5.110.4.tgz +0 -0
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.110%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22936e008b28%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22c0d9975d75%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%227b1858c545%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%227f25cf5ec5%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22a276e5a86b%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.111%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2200504b8fb4%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%2215e68d0314%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22d2bf2d8e82%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22c055bcc64c%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22d66064f4d7%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -58,7 +58,7 @@
58
58
 
59
59
  <script src="assets/vendor-fca15534b8426c0567400113c63a3e21.js"></script>
60
60
  <script src="assets/chunk.874.461cb3cf5b6b36915f8c.js"></script>
61
- <script src="assets/chunk.524.3096e68df5b51dacf872.js"></script>
62
- <script src="assets/ghost-98d002d50a5e01d2100b2c387a849249.js"></script>
61
+ <script src="assets/chunk.524.405c43b2cb20553b51d9.js"></script>
62
+ <script src="assets/ghost-87cffc153ec73d217c1ae9f9207ea5e1.js"></script>
63
63
  </body>
64
64
  </html>
package/core/demo.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.demo = demo;
4
+ function demo() {
5
+ // nothing to see here
6
+ }
package/core/demo.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function demo() {
2
+ // nothing to see here
3
+ }
@@ -1,5 +1,5 @@
1
1
  const DomainEvents = require('@tryghost/domain-events');
2
- const {URLResourceUpdatedEvent} = require('@tryghost/dynamic-routing-events');
2
+ const URLResourceUpdatedEvent = require('../../../shared/events/URLResourceUpdatedEvent');
3
3
  const IndexMapGenerator = require('./SiteMapIndexGenerator');
4
4
  const PagesMapGenerator = require('./PageMapGenerator');
5
5
  const PostsMapGenerator = require('./PostMapGenerator');
@@ -66,7 +66,7 @@
66
66
 
67
67
 
68
68
  .kg-cta-sponsor-label p span:not(a span) {
69
- color: color-mix(in srgb, currentColor 40%, transparent);
69
+ color: color-mix(in srgb, currentColor 45%, transparent);
70
70
  }
71
71
 
72
72
  .kg-cta-sponsor-label a {
@@ -170,17 +170,25 @@ a.kg-cta-button {
170
170
  display: flex;
171
171
  position: static;
172
172
  align-items: center;
173
- padding: 0 2rem;
174
- height: 2.4em;
175
173
  justify-content: center;
176
174
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
177
- font-size: 0.95em;
178
- font-weight: 600;
175
+ font-weight: 500;
176
+ line-height: 1.65;
179
177
  text-decoration: none;
180
178
  border-radius: 6px;
181
179
  transition: opacity 0.2s ease-in-out;
182
180
  }
183
181
 
182
+ .kg-cta-minimal a.kg-cta-button {
183
+ padding: 0 2rem;
184
+ height: 2.4em;
185
+ font-size: 0.95em;
186
+ }
187
+
188
+ .kg-cta-immersive a.kg-cta-button {
189
+ padding: .8rem 2rem;
190
+ }
191
+
184
192
  a.kg-cta-button:hover {
185
193
  opacity: 0.85;
186
194
  }
@@ -85,6 +85,7 @@
85
85
  width: 100%;
86
86
  z-index: 9999;
87
87
  padding: 12px 16px;
88
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
88
89
  }
89
90
 
90
91
  .kg-video-current-time {
@@ -7,11 +7,12 @@ const labs = require('../../../shared/labs');
7
7
  const getCaptchaSettings = () => {
8
8
  if (labs.isSet('captcha')) {
9
9
  return {
10
- captcha_enabled: config.get('captcha:enabled'),
11
10
  captcha_sitekey: config.get('captcha:siteKey')
12
11
  };
13
12
  } else {
14
- return {};
13
+ return {
14
+ captcha_enabled: false
15
+ };
15
16
  }
16
17
  };
17
18
 
@@ -75,7 +75,8 @@ const EDITABLE_SETTINGS = [
75
75
  'recommendations_enabled',
76
76
  'body_font',
77
77
  'heading_font',
78
- 'blocked_email_domains'
78
+ 'blocked_email_domains',
79
+ 'captcha_enabled'
79
80
  ];
80
81
 
81
82
  module.exports = {
@@ -0,0 +1,8 @@
1
+ const {addSetting} = require('../../utils');
2
+
3
+ module.exports = addSetting({
4
+ key: 'captcha_enabled',
5
+ value: 'false',
6
+ type: 'boolean',
7
+ group: 'members'
8
+ });
@@ -319,6 +319,14 @@
319
319
  "blocked_email_domains": {
320
320
  "defaultValue": "[]",
321
321
  "type": "array"
322
+ },
323
+ "captcha_enabled": {
324
+ "defaultValue": "false",
325
+ "validations": {
326
+ "isEmpty": false,
327
+ "isIn": [["true", "false"]]
328
+ },
329
+ "type": "boolean"
322
330
  }
323
331
  },
324
332
  "portal": {
@@ -0,0 +1,25 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+
3
+ module.exports = class ClickEvent {
4
+ /** @type {ObjectID} */
5
+ event_id;
6
+ /** @type {string} */
7
+ member_uuid;
8
+ /** @type {ObjectID} */
9
+ link_id;
10
+
11
+ constructor(data) {
12
+ if (!data.id) {
13
+ this.event_id = new ObjectID();
14
+ }
15
+
16
+ if (typeof data.id === 'string') {
17
+ this.event_id = ObjectID.createFromHexString(data.id);
18
+ } else {
19
+ this.event_id = data.id;
20
+ }
21
+
22
+ this.member_uuid = data.member_uuid;
23
+ this.link_id = data.link_id;
24
+ }
25
+ };
@@ -0,0 +1,36 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+
3
+ /**
4
+ * @typedef {Object} FullPostLinkCount
5
+ * @property {number} clicks
6
+ */
7
+
8
+ /**
9
+ * Stores the connection between a LinkRedirect and a Post
10
+ */
11
+ module.exports = class FullPostLink {
12
+ /** @type {ObjectID} */
13
+ post_id;
14
+
15
+ /** @type {import('@tryghost/link-redirects/lib/LinkRedirect')} */
16
+ link;
17
+
18
+ /** @type {FullPostLinkCount} */
19
+ count;
20
+
21
+ /**
22
+ * @param {object} data
23
+ * @param {string|ObjectID} data.post_id
24
+ * @param {import('@tryghost/link-redirects/lib/LinkRedirect')} data.link
25
+ * @param {FullPostLinkCount} data.count
26
+ */
27
+ constructor(data) {
28
+ if (typeof data.post_id === 'string') {
29
+ this.post_id = ObjectID.createFromHexString(data.post_id);
30
+ } else {
31
+ this.post_id = data.post_id;
32
+ }
33
+ this.link = data.link;
34
+ this.count = data.count;
35
+ }
36
+ };
@@ -1,4 +1,4 @@
1
- const {LinkClick} = require('@tryghost/link-tracking');
1
+ const LinkClick = require('./ClickEvent');
2
2
  const ObjectID = require('bson-objectid').default;
3
3
  const sentry = require('../../../shared/sentry');
4
4
  const config = require('../../../shared/config');
@@ -0,0 +1,237 @@
1
+ const {RedirectEvent} = require('@tryghost/link-redirects');
2
+ const LinkClick = require('./ClickEvent');
3
+ const PostLink = require('./PostLink');
4
+ const ObjectID = require('bson-objectid').default;
5
+ const errors = require('@tryghost/errors');
6
+ const nql = require('@tryghost/nql');
7
+ const _ = require('lodash');
8
+ const tpl = require('@tryghost/tpl');
9
+ const moment = require('moment');
10
+
11
+ /**
12
+ * @typedef {object} ILinkClickRepository
13
+ * @prop {(event: LinkClick) => Promise<void>} save
14
+ * @prop {({filter: string}) => Promise<LinkClick[]>} getAll
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} ILinkRedirect
19
+ * @prop {ObjectID} link_id
20
+ * @prop {URL} to
21
+ * @prop {URL} from
22
+ */
23
+
24
+ /**
25
+ * @typedef {import('./FullPostLink')} FullPostLink
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} ILinkRedirectService
30
+ * @prop {(to: URL, slug: string) => Promise<ILinkRedirect>} addRedirect
31
+ * @prop {() => Promise<string>} getSlug
32
+ * @prop {({filter: string}) => Promise<ILinkRedirect[]>} getAll
33
+ * @prop {({filter: string}) => Promise<string[]>} getFilteredIds
34
+ */
35
+
36
+ /**
37
+ * @typedef {object} IPostLinkRepository
38
+ * @prop {(postLink: PostLink) => Promise<void>} save
39
+ * @prop {({filter: string}) => Promise<FullPostLink[]>} getAll
40
+ * @prop {(linkIds: array, data, options) => Promise<FullPostLink[]>} updateLinks
41
+ */
42
+
43
+ const messages = {
44
+ invalidFilter: 'Invalid filter value received',
45
+ unsupportedBulkAction: 'Unsupported bulk action',
46
+ invalidRedirectUrl: 'Invalid redirect URL value'
47
+ };
48
+
49
+ class LinkClickTrackingService {
50
+ #initialised = false;
51
+
52
+ /** @type ILinkClickRepository */
53
+ #linkClickRepository;
54
+ /** @type ILinkRedirectService */
55
+ #linkRedirectService;
56
+ /** @type IPostLinkRepository */
57
+ #postLinkRepository;
58
+ /** @type DomainEvents */
59
+ #DomainEvents;
60
+ /** @type {Object} */
61
+ #LinkRedirect;
62
+ /** @type {Object} */
63
+ #urlUtils;
64
+
65
+ /**
66
+ * @param {object} deps
67
+ * @param {ILinkClickRepository} deps.linkClickRepository
68
+ * @param {ILinkRedirectService} deps.linkRedirectService
69
+ * @param {IPostLinkRepository} deps.postLinkRepository
70
+ * @param {DomainEvents} deps.DomainEvents
71
+ * @param {urlUtils} deps.urlUtils
72
+ */
73
+ constructor(deps) {
74
+ this.#linkClickRepository = deps.linkClickRepository;
75
+ this.#linkRedirectService = deps.linkRedirectService;
76
+ this.#postLinkRepository = deps.postLinkRepository;
77
+ this.#DomainEvents = deps.DomainEvents;
78
+ this.#urlUtils = deps.urlUtils;
79
+ }
80
+
81
+ async init() {
82
+ if (this.#initialised) {
83
+ return;
84
+ }
85
+ this.subscribe();
86
+ this.#initialised = true;
87
+ }
88
+
89
+ /**
90
+ * @param {object} options
91
+ * @param {string} options.filter
92
+ * @return {Promise<FullPostLink[]>}
93
+ */
94
+ async getLinks(options) {
95
+ return await this.#postLinkRepository.getAll({
96
+ filter: options.filter
97
+ });
98
+ }
99
+
100
+ /**
101
+ * validate and manage the new redirect url in filter
102
+ * `to` url needs decoding and transformation to relative url for comparision
103
+ * @param {string} filter
104
+ * @returns {Object} parsed filter
105
+ * @throws {errors.BadRequestError}
106
+ */
107
+ #parseLinkFilter(filter) {
108
+ try {
109
+ const filterJson = nql(filter).parse();
110
+ const postId = filterJson?.$and?.[0]?.post_id;
111
+ const redirectUrl = new URL(filterJson?.$and?.[1]?.to);
112
+ if (!postId || !redirectUrl) {
113
+ throw new errors.BadRequestError({
114
+ message: tpl(messages.invalidFilter)
115
+ });
116
+ }
117
+ return {
118
+ postId,
119
+ redirectUrl
120
+ };
121
+ } catch (e) {
122
+ throw new errors.BadRequestError({
123
+ message: tpl(messages.invalidFilter),
124
+ context: e.message
125
+ });
126
+ }
127
+ }
128
+
129
+ #getRedirectLinkWithAttribution({newLink, oldLink, postId}) {
130
+ const newUrl = new URL(newLink);
131
+ const oldUrl = new URL(oldLink);
132
+ // append newsletter ref query param from oldUrl to newUrl
133
+ if (oldUrl.searchParams.has('ref')) {
134
+ newUrl.searchParams.set('ref', oldUrl.searchParams.get('ref'));
135
+ }
136
+
137
+ // append post attribution to site urls
138
+ const isSite = this.#urlUtils.isSiteUrl(newUrl);
139
+ if (isSite) {
140
+ newUrl.searchParams.set('attribution_type', 'post');
141
+ newUrl.searchParams.set('attribution_id', postId);
142
+ }
143
+ return newUrl;
144
+ }
145
+
146
+ async #updateLinks(data, options) {
147
+ const filterOptions = _.pick(options, ['transacting', 'context', 'filter']);
148
+
149
+ // decode and parse filter to manage new redirect url
150
+ const {postId, redirectUrl} = this.#parseLinkFilter(filterOptions.filter);
151
+
152
+ // manages transformation of current url to relative for comparision
153
+ const transformedOldUrl = this.#urlUtils.absoluteToTransformReady(redirectUrl.href);
154
+ const filterQuery = `post_id:'${postId}'+to:'${transformedOldUrl}'`;
155
+
156
+ const updatedFilterOptions = {
157
+ ...filterOptions,
158
+ filter: filterQuery
159
+ };
160
+
161
+ // get new redirect link with proper attribution
162
+ const newRedirectUrl = this.#getRedirectLinkWithAttribution({
163
+ newLink: data.meta?.link?.to,
164
+ oldLink: redirectUrl.href,
165
+ postId
166
+ });
167
+ const linkIds = await this.#linkRedirectService.getFilteredIds(updatedFilterOptions);
168
+
169
+ const bulkUpdateOptions = _.pick(options, ['transacting']);
170
+ const updateData = {
171
+ to: this.#urlUtils.absoluteToTransformReady(newRedirectUrl.href),
172
+ updated_at: moment().format('YYYY-MM-DD HH:mm:ss')
173
+ };
174
+
175
+ return await this.#postLinkRepository.updateLinks(linkIds, updateData, bulkUpdateOptions);
176
+ }
177
+
178
+ async bulkEdit(data, options) {
179
+ if (data.action === 'updateLink') {
180
+ return await this.#updateLinks(data, options);
181
+ }
182
+ throw new errors.IncorrectUsageError({
183
+ message: tpl(messages.unsupportedBulkAction)
184
+ });
185
+ }
186
+
187
+ /**
188
+ * @private (not using # to allow tests)
189
+ * Replace URL with a redirect that redirects to the original URL, and link that redirect with the given post
190
+ */
191
+ async addRedirectToUrl(url, post) {
192
+ // Generate a unique redirect slug
193
+ const slugUrl = await this.#linkRedirectService.getSlugUrl();
194
+
195
+ // Add redirect for link click tracking
196
+ const redirect = await this.#linkRedirectService.addRedirect(slugUrl, url);
197
+
198
+ // Store a reference of the link against the post
199
+ const postLink = new PostLink({
200
+ link_id: redirect.link_id,
201
+ post_id: ObjectID.createFromHexString(post.id)
202
+ });
203
+ await this.#postLinkRepository.save(postLink);
204
+
205
+ return redirect.from;
206
+ }
207
+
208
+ /**
209
+ * Add tracking to a URL and returns a new URL (if link click tracking is enabled)
210
+ * @param {URL} url
211
+ * @param {Post} post
212
+ * @param {string} memberUuid
213
+ * @return {Promise<URL>}
214
+ */
215
+ async addTrackingToUrl(url, post, memberUuid) {
216
+ url = await this.addRedirectToUrl(url, post);
217
+ url.searchParams.set('m', memberUuid);
218
+ return url;
219
+ }
220
+
221
+ subscribe() {
222
+ this.#DomainEvents.subscribe(RedirectEvent, async (event) => {
223
+ const uuid = event.data.url.searchParams.get('m');
224
+ if (!uuid) {
225
+ return;
226
+ }
227
+
228
+ const click = new LinkClick({
229
+ member_uuid: uuid,
230
+ link_id: event.data.link.link_id
231
+ });
232
+ await this.#linkClickRepository.save(click);
233
+ });
234
+ }
235
+ }
236
+
237
+ module.exports = LinkClickTrackingService;
@@ -0,0 +1,29 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+
3
+ /**
4
+ * Stores the connection between a LinkRedirect and a Post
5
+ */
6
+ module.exports = class PostLink {
7
+ /** @type {ObjectID} */
8
+ post_id;
9
+ /** @type {ObjectID} */
10
+ link_id;
11
+
12
+ /**
13
+ * @param {object} data
14
+ * @param {string|ObjectID} data.post_id
15
+ * @param {string|ObjectID} data.link_id
16
+ */
17
+ constructor(data) {
18
+ if (typeof data.post_id === 'string') {
19
+ this.post_id = ObjectID.createFromHexString(data.post_id);
20
+ } else {
21
+ this.post_id = data.post_id;
22
+ }
23
+ if (typeof data.link_id === 'string') {
24
+ this.link_id = ObjectID.createFromHexString(data.link_id);
25
+ } else {
26
+ this.link_id = data.link_id;
27
+ }
28
+ }
29
+ };
@@ -1,9 +1,9 @@
1
- const {FullPostLink} = require('@tryghost/link-tracking');
1
+ const FullPostLink = require('./FullPostLink');
2
2
  const _ = require('lodash');
3
3
 
4
4
  /**
5
5
  * @typedef {import('bson-objectid').default} ObjectID
6
- * @typedef {import('@tryghost/link-tracking/lib/PostLink')} PostLink
6
+ * @typedef {import('./PostLink')} PostLink
7
7
  */
8
8
 
9
9
  module.exports = class PostLinkRepository {
@@ -20,7 +20,7 @@ class LinkTrackingServiceWrapper {
20
20
  const {MemberLinkClickEvent} = require('@tryghost/member-events');
21
21
  const DomainEvents = require('@tryghost/domain-events');
22
22
 
23
- const {LinkClickTrackingService} = require('@tryghost/link-tracking');
23
+ const LinkClickTrackingService = require('./LinkClickTrackingService');
24
24
 
25
25
  const postLinkRepository = new PostLinkRepository({
26
26
  LinkRedirect: models.Redirect,
@@ -0,0 +1,61 @@
1
+ const {MemberCreatedEvent, SubscriptionCreatedEvent} = require('@tryghost/member-events');
2
+
3
+ /**
4
+ * Store events in the database
5
+ */
6
+ class EventStorage {
7
+ /**
8
+ *
9
+ * @param {Object} deps
10
+ * @param {Object} deps.labsService
11
+ * @param {Object} deps.models
12
+ * @param {Object} deps.models.MemberCreatedEvent
13
+ * @param {Object} deps.models.SubscriptionCreatedEvent
14
+ */
15
+ constructor({labsService, models}) {
16
+ this.models = models;
17
+ this.labsService = labsService;
18
+ }
19
+
20
+ /**
21
+ * Subscribe to events of this domainEvents service
22
+ * @param {Object} domainEvents The DomainEvents service
23
+ */
24
+ subscribe(domainEvents) {
25
+ domainEvents.subscribe(MemberCreatedEvent, async (event) => {
26
+ let attribution = event.data.attribution;
27
+
28
+ await this.models.MemberCreatedEvent.add({
29
+ member_id: event.data.memberId,
30
+ created_at: event.timestamp,
31
+ attribution_id: attribution?.id ?? null,
32
+ attribution_url: attribution?.url ?? null,
33
+ attribution_type: attribution?.type ?? null,
34
+ source: event.data.source,
35
+ referrer_source: attribution?.referrerSource ?? null,
36
+ referrer_medium: attribution?.referrerMedium ?? null,
37
+ referrer_url: attribution?.referrerUrl ?? null,
38
+ batch_id: event.data.batchId ?? null
39
+ });
40
+ });
41
+
42
+ domainEvents.subscribe(SubscriptionCreatedEvent, async (event) => {
43
+ let attribution = event.data.attribution;
44
+
45
+ await this.models.SubscriptionCreatedEvent.add({
46
+ member_id: event.data.memberId,
47
+ subscription_id: event.data.subscriptionId,
48
+ created_at: event.timestamp,
49
+ attribution_id: attribution?.id ?? null,
50
+ attribution_url: attribution?.url ?? null,
51
+ attribution_type: attribution?.type ?? null,
52
+ referrer_source: attribution?.referrerSource ?? null,
53
+ referrer_medium: attribution?.referrerMedium ?? null,
54
+ referrer_url: attribution?.referrerUrl ?? null,
55
+ batch_id: event.data.batchId ?? null
56
+ });
57
+ });
58
+ }
59
+ }
60
+
61
+ module.exports = EventStorage;
@@ -0,0 +1,96 @@
1
+ const moment = require('moment-timezone');
2
+
3
+ /**
4
+ * A cache that stores the member ids that have been seen today. This cache is used to avoid having to query the database for the last_seen_at timestamp of a member multiple times in the same day.
5
+ *
6
+ * @constructor
7
+ * @param {Object} settingsCache - An instance of the settings cache
8
+ * @property {Set} _cache - A set that stores all the member ids that have been seen today
9
+ * @property {Object} _settingsCache - An instance of the settings cache
10
+ * @property {string} _startOfDay - The start of the current day in the site timezone, formatted in ISO 8601
11
+ */
12
+ class LastSeenAtCache {
13
+ /**
14
+ *
15
+ * @param {Object} deps - Dependencies
16
+ * @param {Object} deps.services - The list of service dependencies
17
+ * @param {Object} deps.services.settingsCache - The settings service
18
+ */
19
+ constructor({services: {settingsCache}}) {
20
+ this._cache = new Set();
21
+ this._settingsCache = settingsCache;
22
+ this._startOfDay = this._getStartOfCurrentDay();
23
+ }
24
+
25
+ /**
26
+ * @method add - Adds a member id to the cache
27
+ * @param {string} memberId
28
+ */
29
+ add(memberId) {
30
+ this._cache.add(memberId);
31
+ }
32
+
33
+ /**
34
+ * @method remove - Removes a member id from the cache
35
+ * @param {string} memberId
36
+ */
37
+ remove(memberId) {
38
+ this._cache.delete(memberId);
39
+ }
40
+
41
+ /**
42
+ * @method shouldUpdateMember - Checks if a member should be updated
43
+ * @param {string} memberId
44
+ * @returns {boolean} - Returns true if the member should be updated
45
+ */
46
+ shouldUpdateMember(memberId) {
47
+ return !this._has(memberId);
48
+ }
49
+
50
+ /**
51
+ * @method clear - Clears the cache
52
+ */
53
+ clear() {
54
+ this._cache.clear();
55
+ }
56
+
57
+ /**
58
+ * @method _has - Refreshes the cache and checks if a member id is in the cache
59
+ * @param {string} memberId
60
+ * @returns {boolean}
61
+ */
62
+ _has(memberId) {
63
+ this._refresh();
64
+ return this._cache.has(memberId);
65
+ }
66
+
67
+ /**
68
+ * @method _shouldClear - Checks if the cache should be cleared, based on the current day
69
+ * @returns {boolean} - Returns true if the cache should be cleared
70
+ */
71
+ _shouldClear() {
72
+ return this._startOfDay !== this._getStartOfCurrentDay();
73
+ }
74
+
75
+ /**
76
+ * @method _refresh - Clears the cache if the day has changed
77
+ */
78
+ _refresh() {
79
+ if (this._shouldClear()) {
80
+ this.clear();
81
+ this._startOfDay = this._getStartOfCurrentDay();
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Returns the start of the current day in the site timezone
87
+ * @returns {string} The start of the current day in the site timezone, formatted as a ISO 8601 string
88
+ */
89
+ _getStartOfCurrentDay() {
90
+ const timezone = this._settingsCache.get('timezone') || 'Etc/UTC';
91
+ const startOfDay = moment().tz(timezone).startOf('day').utc().toISOString();
92
+ return startOfDay;
93
+ }
94
+ }
95
+
96
+ module.exports = LastSeenAtCache;