ghost 5.14.2 → 5.16.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 (176) hide show
  1. package/components/tryghost-adapter-manager-5.16.0.tgz +0 -0
  2. package/components/tryghost-api-framework-5.16.0.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-5.16.0.tgz +0 -0
  4. package/components/tryghost-bootstrap-socket-5.16.0.tgz +0 -0
  5. package/components/tryghost-constants-5.16.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-5.16.0.tgz +0 -0
  7. package/components/tryghost-domain-events-5.16.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.16.0.tgz +0 -0
  9. package/components/{tryghost-email-analytics-service-5.14.2.tgz → tryghost-email-analytics-service-5.16.0.tgz} +0 -0
  10. package/components/tryghost-email-content-generator-5.16.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.16.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.16.0.tgz +0 -0
  13. package/components/tryghost-html-to-plaintext-5.16.0.tgz +0 -0
  14. package/components/{tryghost-job-manager-5.14.2.tgz → tryghost-job-manager-5.16.0.tgz} +0 -0
  15. package/components/tryghost-link-redirects-5.16.0.tgz +0 -0
  16. package/components/tryghost-link-replacer-5.16.0.tgz +0 -0
  17. package/components/tryghost-link-tracking-5.16.0.tgz +0 -0
  18. package/components/tryghost-magic-link-5.16.0.tgz +0 -0
  19. package/components/{tryghost-mailgun-client-5.14.2.tgz → tryghost-mailgun-client-5.16.0.tgz} +0 -0
  20. package/components/tryghost-member-analytics-service-5.16.0.tgz +0 -0
  21. package/components/tryghost-member-attribution-5.16.0.tgz +0 -0
  22. package/components/tryghost-member-events-5.16.0.tgz +0 -0
  23. package/components/tryghost-members-analytics-ingress-5.16.0.tgz +0 -0
  24. package/components/tryghost-members-api-5.16.0.tgz +0 -0
  25. package/components/tryghost-members-csv-5.16.0.tgz +0 -0
  26. package/components/tryghost-members-events-service-5.16.0.tgz +0 -0
  27. package/components/tryghost-members-importer-5.16.0.tgz +0 -0
  28. package/components/tryghost-members-offers-5.16.0.tgz +0 -0
  29. package/components/{tryghost-members-payments-5.14.2.tgz → tryghost-members-payments-5.16.0.tgz} +0 -0
  30. package/components/{tryghost-members-ssr-5.14.2.tgz → tryghost-members-ssr-5.16.0.tgz} +0 -0
  31. package/components/tryghost-members-stripe-service-5.16.0.tgz +0 -0
  32. package/components/tryghost-minifier-5.16.0.tgz +0 -0
  33. package/components/tryghost-mw-api-version-mismatch-5.16.0.tgz +0 -0
  34. package/components/tryghost-mw-cache-control-5.16.0.tgz +0 -0
  35. package/components/{tryghost-mw-error-handler-5.14.2.tgz → tryghost-mw-error-handler-5.16.0.tgz} +0 -0
  36. package/components/tryghost-mw-session-from-token-5.16.0.tgz +0 -0
  37. package/components/tryghost-mw-update-user-last-seen-5.16.0.tgz +0 -0
  38. package/components/tryghost-mw-vhost-5.16.0.tgz +0 -0
  39. package/components/{tryghost-oembed-service-5.14.2.tgz → tryghost-oembed-service-5.16.0.tgz} +0 -0
  40. package/components/{tryghost-package-json-5.14.2.tgz → tryghost-package-json-5.16.0.tgz} +0 -0
  41. package/components/tryghost-referrers-5.16.0.tgz +0 -0
  42. package/components/{tryghost-security-5.14.2.tgz → tryghost-security-5.16.0.tgz} +0 -0
  43. package/components/tryghost-session-service-5.16.0.tgz +0 -0
  44. package/components/tryghost-settings-path-manager-5.16.0.tgz +0 -0
  45. package/components/tryghost-staff-service-5.16.0.tgz +0 -0
  46. package/components/tryghost-stats-service-5.16.0.tgz +0 -0
  47. package/components/tryghost-update-check-service-5.16.0.tgz +0 -0
  48. package/components/{tryghost-verification-trigger-5.14.2.tgz → tryghost-verification-trigger-5.16.0.tgz} +0 -0
  49. package/components/{tryghost-version-notifications-data-service-5.14.2.tgz → tryghost-version-notifications-data-service-5.16.0.tgz} +0 -0
  50. package/content/themes/casper/default.hbs +2 -2
  51. package/core/boot.js +10 -3
  52. package/core/built/admin/assets/{chunk.143.a5ef705453da0d58b75a.js → chunk.143.a281d460e6059cd0210a.js} +21 -21
  53. package/core/built/admin/assets/{chunk.174.2edaa0869bfc2d88cf90.js → chunk.174.e1e89637eab79fdd5c5d.js} +68 -68
  54. package/core/built/admin/assets/{chunk.178.579a6edabc75a2d7378f.js → chunk.178.68eca2346b6f343991e7.js} +4 -4
  55. package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js → chunk.579.d14c3688558f34afeb3e.js} +8872 -7851
  56. package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js.LICENSE.txt → chunk.579.d14c3688558f34afeb3e.js.LICENSE.txt} +45 -0
  57. package/core/built/admin/assets/fonts/{Inter.ttf → Inter-e19174fb2c0e19b1fa67492a07886c75.ttf} +0 -0
  58. package/core/built/admin/assets/ghost-6491d134c450ca676911ea17e16cd7d4.css +1 -0
  59. package/core/built/admin/assets/ghost-dark-297ab2fcf4cadd1c950b84089a38c5e2.css +1 -0
  60. package/core/built/admin/assets/{ghost-8919656440ad4617a07bb31069b1f71b.js → ghost-f2bf99b26aee662cf37fe59f87b1ceb5.js} +593 -511
  61. package/core/built/admin/assets/img/{amp.svg → amp-d7b72aae3315fda95921fb575dfca100.svg} +0 -0
  62. package/core/built/admin/assets/img/{disqus.svg → disqus-43503a3fa4f38dc8c61c7358b811f343.svg} +0 -0
  63. package/core/built/admin/assets/img/{favicon.ico → favicon-a9c6dbdcdc3ae568f4e0dad92149a0e3.ico} +0 -0
  64. package/core/built/admin/assets/img/{github.svg → github-c3a739c59df26fed12c10ffb00b33bd4.svg} +0 -0
  65. package/core/built/admin/assets/img/{google-docs.svg → google-docs-1e42cc272fc088da49e4b0ddfb01b006.svg} +0 -0
  66. package/core/built/admin/assets/img/{mailchimp.svg → mailchimp-f22b1e130aac764965b9306d7265a6b2.svg} +0 -0
  67. package/core/built/admin/assets/img/marketing/analytics-1-aa2d72c4e7347a3cb5666d07916b92aa.jpg +0 -0
  68. package/core/built/admin/assets/img/marketing/analytics-2-389d53f80041ff98111cce79facf66b8.jpg +0 -0
  69. package/core/built/admin/assets/img/{patreon.svg → patreon-b19a5e6418a72977a16b30039d374d04.svg} +0 -0
  70. package/core/built/admin/assets/img/{paypal.svg → paypal-38e9448ce7549ea4caf8e7753ae661d6.svg} +0 -0
  71. package/core/built/admin/assets/img/{twitter.svg → twitter-7a7a0ba12d9b5bfb8a2058764a827c31.svg} +0 -0
  72. package/core/built/admin/assets/img/{typeform.svg → typeform-9f23f8712d776a7515594676285266f5.svg} +0 -0
  73. package/core/built/admin/assets/img/{unsplash.svg → unsplash-5b329eef0b11447b4117eaf817ebad6f.svg} +0 -0
  74. package/core/built/admin/assets/img/{zapier.svg → zapier-bf93bc440a3fd43b73489a63c215cdc7.svg} +0 -0
  75. package/core/built/admin/assets/img/{zapier-logo.svg → zapier-logo-a125f24313dfe01ef49af01fc90061fb.svg} +0 -0
  76. package/core/built/admin/assets/{vendor-eb76d0236a09b8b6f44675dba45b1fc6.js → vendor-b2375e2f383cbc3fd73340c4b656c993.js} +59 -47
  77. package/core/built/admin/assets/videos/logo-loader.mp4 +0 -0
  78. package/core/built/admin/index.html +11 -8
  79. package/core/frontend/helpers/search.js +1 -15
  80. package/core/frontend/src/member-attribution/member-attribution.js +64 -3
  81. package/core/frontend/web/site.js +10 -7
  82. package/core/server/api/endpoints/index.js +4 -0
  83. package/core/server/api/endpoints/links.js +25 -0
  84. package/core/server/api/endpoints/posts.js +2 -1
  85. package/core/server/api/endpoints/redirects.js +6 -8
  86. package/core/server/api/endpoints/stats.js +24 -0
  87. package/core/server/api/endpoints/utils/permissions.js +2 -16
  88. package/core/server/api/endpoints/utils/serializers/input/pages.js +5 -5
  89. package/core/server/api/endpoints/utils/serializers/input/posts.js +13 -8
  90. package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
  91. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +51 -0
  92. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +10 -1
  93. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +1 -1
  94. package/core/server/api/endpoints/utils/validators/input/pages.js +24 -9
  95. package/core/server/api/endpoints/utils/validators/input/posts.js +24 -9
  96. package/core/server/data/exporter/table-lists.js +4 -1
  97. package/core/server/data/migrations/utils/settings.js +1 -3
  98. package/core/server/data/migrations/versions/5.15/2022-09-12-16-10-add-posts-lexical-column.js +8 -0
  99. package/core/server/data/migrations/versions/5.15/2022-09-14-12-46-add-email-track-clicks-setting.js +8 -0
  100. package/core/server/data/migrations/versions/5.15/2022-09-16-08-22-add-post-revisions-table.js +9 -0
  101. package/core/server/data/migrations/versions/5.16/2022-09-19-09-04-add-link-redirects-table.js +10 -0
  102. package/core/server/data/migrations/versions/5.16/2022-09-19-09-05-add-members-link-click-events-table.js +8 -0
  103. package/core/server/data/migrations/versions/5.16/2022-09-19-17-44-add-referrer-columns-to-member-events-table.js +21 -0
  104. package/core/server/data/migrations/versions/5.16/2022-09-19-17-44-add-referrer-columns-to-subscription-events-table.js +21 -0
  105. package/core/server/data/schema/default-settings/default-settings.json +8 -0
  106. package/core/server/data/schema/schema.js +29 -1
  107. package/core/server/lib/lexical.js +12 -0
  108. package/core/server/models/base/plugins/user-type.js +4 -6
  109. package/core/server/models/link-redirect.js +65 -0
  110. package/core/server/models/member-link-click-event.js +26 -0
  111. package/core/server/models/post-revision.js +35 -0
  112. package/core/server/models/post.js +90 -9
  113. package/core/server/services/bulk-email/bulk-email-processor.js +9 -10
  114. package/core/server/services/{redirects → custom-redirects}/api.js +0 -0
  115. package/core/server/services/{redirects → custom-redirects}/index.js +0 -0
  116. package/core/server/services/{redirects → custom-redirects}/utils.js +0 -0
  117. package/core/server/services/{redirects → custom-redirects}/validation.js +0 -0
  118. package/core/server/services/explore/service.js +5 -3
  119. package/core/server/services/link-redirection/LinkRedirectRepository.js +88 -0
  120. package/core/server/services/link-redirection/index.js +31 -0
  121. package/core/server/services/link-tracking/LinkClickRepository.js +69 -0
  122. package/core/server/services/link-tracking/PostLinkRepository.js +62 -0
  123. package/core/server/services/link-tracking/index.js +48 -0
  124. package/core/server/services/mega/email-preview.js +7 -0
  125. package/core/server/services/mega/mega.js +1 -1
  126. package/core/server/services/mega/post-email-serializer.js +101 -27
  127. package/core/server/services/member-attribution/index.js +12 -5
  128. package/core/server/services/members/api.js +1 -2
  129. package/core/server/services/permissions/index.js +1 -2
  130. package/core/server/services/posts/posts-service.js +7 -16
  131. package/core/server/services/posts/stats/post-stats.js +35 -0
  132. package/core/server/services/staff/index.js +10 -1
  133. package/core/server/services/url/config.js +2 -0
  134. package/core/server/web/admin/app.js +8 -2
  135. package/core/server/web/api/endpoints/admin/routes.js +5 -0
  136. package/core/shared/config/defaults.json +7 -7
  137. package/core/shared/config/overrides.json +3 -2
  138. package/core/shared/labs.js +4 -2
  139. package/package.json +115 -107
  140. package/yarn.lock +828 -414
  141. package/components/tryghost-adapter-manager-5.14.2.tgz +0 -0
  142. package/components/tryghost-api-framework-5.14.2.tgz +0 -0
  143. package/components/tryghost-api-version-compatibility-service-5.14.2.tgz +0 -0
  144. package/components/tryghost-bootstrap-socket-5.14.2.tgz +0 -0
  145. package/components/tryghost-constants-5.14.2.tgz +0 -0
  146. package/components/tryghost-custom-theme-settings-service-5.14.2.tgz +0 -0
  147. package/components/tryghost-domain-events-5.14.2.tgz +0 -0
  148. package/components/tryghost-email-analytics-provider-mailgun-5.14.2.tgz +0 -0
  149. package/components/tryghost-email-content-generator-5.14.2.tgz +0 -0
  150. package/components/tryghost-express-dynamic-redirects-5.14.2.tgz +0 -0
  151. package/components/tryghost-extract-api-key-5.14.2.tgz +0 -0
  152. package/components/tryghost-html-to-plaintext-5.14.2.tgz +0 -0
  153. package/components/tryghost-magic-link-5.14.2.tgz +0 -0
  154. package/components/tryghost-member-analytics-service-5.14.2.tgz +0 -0
  155. package/components/tryghost-member-attribution-5.14.2.tgz +0 -0
  156. package/components/tryghost-member-events-5.14.2.tgz +0 -0
  157. package/components/tryghost-members-analytics-ingress-5.14.2.tgz +0 -0
  158. package/components/tryghost-members-api-5.14.2.tgz +0 -0
  159. package/components/tryghost-members-csv-5.14.2.tgz +0 -0
  160. package/components/tryghost-members-events-service-5.14.2.tgz +0 -0
  161. package/components/tryghost-members-importer-5.14.2.tgz +0 -0
  162. package/components/tryghost-members-offers-5.14.2.tgz +0 -0
  163. package/components/tryghost-members-stripe-service-5.14.2.tgz +0 -0
  164. package/components/tryghost-minifier-5.14.2.tgz +0 -0
  165. package/components/tryghost-mw-api-version-mismatch-5.14.2.tgz +0 -0
  166. package/components/tryghost-mw-cache-control-5.14.2.tgz +0 -0
  167. package/components/tryghost-mw-session-from-token-5.14.2.tgz +0 -0
  168. package/components/tryghost-mw-update-user-last-seen-5.14.2.tgz +0 -0
  169. package/components/tryghost-mw-vhost-5.14.2.tgz +0 -0
  170. package/components/tryghost-session-service-5.14.2.tgz +0 -0
  171. package/components/tryghost-settings-path-manager-5.14.2.tgz +0 -0
  172. package/components/tryghost-staff-service-5.14.2.tgz +0 -0
  173. package/components/tryghost-update-check-service-5.14.2.tgz +0 -0
  174. package/core/built/admin/assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css +0 -1
  175. package/core/built/admin/assets/ghost-dark-13b669d50f494edf24d832b32ece2177.css +0 -1
  176. package/core/server/services/permissions/public.js +0 -76
@@ -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%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.14%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%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%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%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.16%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%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%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -20,7 +20,7 @@
20
20
  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
21
21
  <meta name="apple-mobile-web-app-title" content="Ghost" />
22
22
 
23
- <link rel="shortcut icon" href="assets/img/favicon.ico" />
23
+ <link rel="shortcut icon" href="assets/img/favicon-a9c6dbdcdc3ae568f4e0dad92149a0e3.ico" />
24
24
  <link rel="apple-touch-icon" href="assets/img/apple-touch-icon-74680e326a7e87b159d366c7d4fb3d4b.png" />
25
25
 
26
26
  <meta name="application-name" content="Ghost" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-733135cd6cbca8126c6fa223d63a5bf3.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-6491d134c450ca676911ea17e16cd7d4.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -45,7 +45,10 @@
45
45
 
46
46
  <div class="ember-load-indicator">
47
47
  <div class="gh-loading-content">
48
- <div class="gh-loading-spinner"></div>
48
+ <video width="100" height="100" loop="" autoplay="" muted="" playsinline="" preload="metadata">
49
+ <source src="assets/videos/logo-loader.mp4" type="video/mp4" />
50
+ <div class="gh-loading-spinner"></div>
51
+ </video>
49
52
  </div>
50
53
  </div>
51
54
 
@@ -53,9 +56,9 @@
53
56
 
54
57
  <div id="ember-basic-dropdown-wormhole"></div>
55
58
 
56
- <script src="assets/vendor-eb76d0236a09b8b6f44675dba45b1fc6.js"></script>
57
- <script src="assets/chunk.579.2de3f4300baf25f9a0db.js"></script>
58
- <script src="assets/chunk.143.a5ef705453da0d58b75a.js"></script>
59
- <script src="assets/ghost-8919656440ad4617a07bb31069b1f71b.js"></script>
59
+ <script src="assets/vendor-b2375e2f383cbc3fd73340c4b656c993.js"></script>
60
+ <script src="assets/chunk.579.d14c3688558f34afeb3e.js"></script>
61
+ <script src="assets/chunk.143.a281d460e6059cd0210a.js"></script>
62
+ <script src="assets/ghost-f2bf99b26aee662cf37fe59f87b1ceb5.js"></script>
60
63
  </body>
61
64
  </html>
@@ -1,9 +1,8 @@
1
1
  // # search helper
2
2
 
3
3
  const {SafeString} = require('../services/handlebars');
4
- const {labs} = require('../services/proxy');
5
4
 
6
- function search() {
5
+ module.exports = function search() {
7
6
  // We want this to output as one line, but splitting for readability
8
7
  const svg = '<button class="gh-search-icon" aria-label="search" data-ghost-search '
9
8
  + 'style="display: inline-flex; justify-content: center; align-items: center; width: 32px; height: 32px; padding: 0; border: 0; color: inherit; background-color: transparent; cursor: pointer; outline: none;">'
@@ -11,17 +10,4 @@ function search() {
11
10
  + '<path d="M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm-9 7a9 9 0 1 1 18 0 9 9 0 0 1-18 0Z" fill="currentColor"/></svg></button>';
12
11
 
13
12
  return new SafeString(svg);
14
- }
15
-
16
- module.exports = function searchLabsWrapper() {
17
- let self = this;
18
- let args = arguments;
19
-
20
- return labs.enabledHelper({
21
- flagKey: 'searchHelper',
22
- flagName: 'Search helper',
23
- helperName: 'search'
24
- }, () => {
25
- return search.apply(self, args); // eslint-disable-line camelcase
26
- });
27
13
  };
@@ -15,6 +15,11 @@ const LIMIT = 15;
15
15
  // "path": "/about/"
16
16
  // },
17
17
  // {
18
+ // "time": 12341234,
19
+ // "id": "manually-added-id",
20
+ // "type": "post",
21
+ // },
22
+ // {
18
23
  // "time": 12341235,
19
24
  // "path": "/welcome/"
20
25
  // }
@@ -57,7 +62,7 @@ const LIMIT = 15;
57
62
  // Valid item (so all following items are also valid by definition)
58
63
  return true;
59
64
  });
60
-
65
+
61
66
  if (firstNotExpiredIndex > 0) {
62
67
  // Remove until the first valid item
63
68
  history.splice(0, firstNotExpiredIndex);
@@ -66,17 +71,73 @@ const LIMIT = 15;
66
71
  history = [];
67
72
  }
68
73
 
74
+ // Fetch referrer data from query params
75
+ let refParam;
76
+ let sourceParam;
77
+ let utmSourceParam;
78
+ let utmMediumParam;
79
+ try {
80
+ // Fetch source/medium from query param
81
+ const url = new URL(window.location.href);
82
+ refParam = url.searchParams.get('ref');
83
+ sourceParam = url.searchParams.get('source');
84
+ utmSourceParam = url.searchParams.get('utm_source');
85
+ utmMediumParam = url.searchParams.get('utm_medium');
86
+ } catch (e) {
87
+ console.error('[Member Attribution] Parsing referrer from querystring failed', e);
88
+ }
89
+
90
+ const refSource = refParam || sourceParam || utmSourceParam || null;
91
+ const refMedium = utmMediumParam || null;
92
+ const refUrl = window.document.referrer || null;
93
+
94
+ // Do we have attributions in the query string?
95
+ try {
96
+ const url = new URL(window.location.href);
97
+ const params = url.searchParams;
98
+ if (params.get('attribution_id') && params.get('attribution_type')) {
99
+ // Add attribution to history before the current path
100
+ history.push({
101
+ time: currentTime,
102
+ id: params.get('attribution_id'),
103
+ type: params.get('attribution_type'),
104
+ refSource,
105
+ refMedium,
106
+ refUrl
107
+ });
108
+
109
+ // Remove attribution from query string
110
+ params.delete('attribution_id');
111
+ params.delete('attribution_type');
112
+ url.search = '?' + params.toString();
113
+ window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`);
114
+ }
115
+ } catch (error) {
116
+ console.error('[Member Attribution] Parsing attribution from querystring failed', error);
117
+ }
118
+
69
119
  const currentPath = window.location.pathname;
70
120
 
71
121
  if (history.length === 0 || history[history.length - 1].path !== currentPath) {
72
122
  history.push({
73
123
  path: currentPath,
74
- time: currentTime
124
+ time: currentTime,
125
+ refSource,
126
+ refMedium,
127
+ refUrl
75
128
  });
76
129
  } else if (history.length > 0) {
77
130
  history[history.length - 1].time = currentTime;
131
+ // Update referrer information for same path if available (e.g. when opening a link on same path via external referrer)
132
+ if (refSource) {
133
+ history[history.length - 1].refSource = refSource;
134
+ history[history.length - 1].refMedium = refMedium;
135
+ }
136
+ if (refUrl) {
137
+ history[history.length - 1].refUrl = refUrl;
138
+ }
78
139
  }
79
-
140
+
80
141
  // Restrict length
81
142
  if (history.length > LIMIT) {
82
143
  history = history.slice(-LIMIT);
@@ -14,7 +14,8 @@ const themeEngine = require('../services/theme-engine');
14
14
  const themeMiddleware = themeEngine.middleware;
15
15
  const membersService = require('../../server/services/members');
16
16
  const offersService = require('../../server/services/offers');
17
- const customRedirects = require('../../server/services/redirects');
17
+ const customRedirects = require('../../server/services/custom-redirects');
18
+ const linkRedirects = require('../../server/services/link-redirection');
18
19
  const siteRoutes = require('./routes');
19
20
  const shared = require('../../server/web/shared');
20
21
  const errorHandler = require('@tryghost/mw-error-handler');
@@ -49,6 +50,8 @@ module.exports = function setupSiteApp(routerConfig) {
49
50
 
50
51
  siteApp.use(offersService.middleware);
51
52
 
53
+ siteApp.use(linkRedirects.service.handleRequest);
54
+
52
55
  // you can extend Ghost with a custom redirects file
53
56
  // see https://github.com/TryGhost/Ghost/issues/7707
54
57
  siteApp.use(customRedirects.middleware);
@@ -78,11 +81,11 @@ module.exports = function setupSiteApp(routerConfig) {
78
81
  // Member attribution
79
82
  siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', constants.ONE_YEAR_S));
80
83
 
81
- // Serve blog images using the storage adapter
84
+ // Serve site images using the storage adapter
82
85
  siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
83
- // Serve blog media using the storage adapter
86
+ // Serve site media using the storage adapter
84
87
  siteApp.use(STATIC_MEDIA_URL_PREFIX, storage.getStorage('media').serve());
85
- // Serve blog files using the storage adapter
88
+ // Serve site files using the storage adapter
86
89
  siteApp.use(STATIC_FILES_URL_PREFIX, storage.getStorage('files').serve());
87
90
 
88
91
  // Global handling for member session, ensures a member is logged in to the frontend
@@ -91,7 +94,7 @@ module.exports = function setupSiteApp(routerConfig) {
91
94
  // /member/.well-known/* serves files (e.g. jwks.json) so it needs to be mounted before the prettyUrl mw to avoid trailing slashes
92
95
  siteApp.use(
93
96
  '/members/.well-known',
94
- shared.middleware.cacheControl('public', {maxAge: 60 * 60 * 24}),
97
+ shared.middleware.cacheControl('public', {maxAge: constants.ONE_DAY_S}),
95
98
  (req, res, next) => membersService.api.middleware.wellKnown(req, res, next)
96
99
  );
97
100
 
@@ -127,7 +130,7 @@ module.exports = function setupSiteApp(routerConfig) {
127
130
 
128
131
  // ### Caching
129
132
  siteApp.use(function (req, res, next) {
130
- // Site frontend is cacheable UNLESS request made by a member or blog is in private mode
133
+ // Site frontend is cacheable UNLESS request made by a member or site is in private mode
131
134
  if (req.member || res.isPrivateBlog) {
132
135
  return shared.middleware.cacheControl('private')(req, res, next);
133
136
  } else {
@@ -148,7 +151,7 @@ module.exports = function setupSiteApp(routerConfig) {
148
151
  router = siteRoutes(routerConfig);
149
152
  Object.setPrototypeOf(SiteRouter, router);
150
153
 
151
- // Set up Frontend routes (including private blogging routes)
154
+ // Set up Frontend routes (including private site routes)
152
155
  siteApp.use(SiteRouter);
153
156
 
154
157
  // ### Error handlers
@@ -185,6 +185,10 @@ module.exports = {
185
185
  return apiFramework.pipeline(require('./comments'), localUtils);
186
186
  },
187
187
 
188
+ get links() {
189
+ return apiFramework.pipeline(require('./links'), localUtils);
190
+ },
191
+
188
192
  /**
189
193
  * Content API Controllers
190
194
  *
@@ -0,0 +1,25 @@
1
+ const linkTrackingService = require('../../services/link-tracking');
2
+
3
+ module.exports = {
4
+ docName: 'links',
5
+ browse: {
6
+ options: [
7
+ 'filter'
8
+ ],
9
+ permissions: false,
10
+ async query(frame) {
11
+ const links = await linkTrackingService.service.getLinks(frame.options);
12
+
13
+ return {
14
+ data: links,
15
+ meta: {
16
+ pagination: {
17
+ total: links.length,
18
+ page: 1,
19
+ pages: 1
20
+ }
21
+ }
22
+ };
23
+ }
24
+ }
25
+ };
@@ -10,7 +10,8 @@ const allowedIncludes = [
10
10
  'tiers',
11
11
  'newsletter',
12
12
  'count.signups',
13
- 'count.conversions'
13
+ 'count.conversions',
14
+ 'count.clicks'
14
15
  ];
15
16
  const unsafeAttrs = ['status', 'authors', 'visibility'];
16
17
 
@@ -1,6 +1,6 @@
1
1
  const path = require('path');
2
2
 
3
- const redirects = require('../../services/redirects');
3
+ const customRedirects = require('../../services/custom-redirects');
4
4
 
5
5
  module.exports = {
6
6
  docName: 'redirects',
@@ -10,11 +10,9 @@ module.exports = {
10
10
  disposition: {
11
11
  type: 'file',
12
12
  value() {
13
- return redirects.api.getRedirectsFilePath()
13
+ return customRedirects.api.getRedirectsFilePath()
14
14
  .then((filePath) => {
15
- // TODO: Default file type is .json for backward compatibility.
16
- // When .yaml becomes default or .json is removed at v4,
17
- // This part should be changed.
15
+ // @deprecated: .json was deprecated in v4.0 but is still the default for backwards compat
18
16
  return filePath === null || path.extname(filePath) === '.json'
19
17
  ? 'redirects.json'
20
18
  : 'redirects.yaml';
@@ -25,13 +23,13 @@ module.exports = {
25
23
  permissions: true,
26
24
  response: {
27
25
  async format() {
28
- const filePath = await redirects.api.getRedirectsFilePath();
26
+ const filePath = await customRedirects.api.getRedirectsFilePath();
29
27
 
30
28
  return filePath === null || path.extname(filePath) === '.json' ? 'json' : 'plain';
31
29
  }
32
30
  },
33
31
  query() {
34
- return redirects.api.get();
32
+ return customRedirects.api.get();
35
33
  }
36
34
  },
37
35
 
@@ -41,7 +39,7 @@ module.exports = {
41
39
  cacheInvalidate: true
42
40
  },
43
41
  query(frame) {
44
- return redirects.api.setFromFilePath(frame.file.path, frame.file.ext);
42
+ return customRedirects.api.setFromFilePath(frame.file.path, frame.file.ext);
45
43
  }
46
44
  }
47
45
  };
@@ -28,5 +28,29 @@ module.exports = {
28
28
  async query() {
29
29
  return await statsService.getSubscriptionCountHistory();
30
30
  }
31
+ },
32
+ postReferrers: {
33
+ data: [
34
+ 'id'
35
+ ],
36
+ permissions: {
37
+ docName: 'posts',
38
+ method: 'browse'
39
+ },
40
+ async query(frame) {
41
+ return await statsService.getPostReferrers(frame.data.id);
42
+ }
43
+ },
44
+ referrersHistory: {
45
+ data: [
46
+ 'id'
47
+ ],
48
+ permissions: {
49
+ docName: 'posts',
50
+ method: 'browse'
51
+ },
52
+ async query() {
53
+ return await statsService.getReferrersHistory();
54
+ }
31
55
  }
32
56
  };
@@ -93,22 +93,8 @@ module.exports = {
93
93
 
94
94
  // CASE: Content API access
95
95
  if (frame.options.context.public && frame.apiType !== 'comments') {
96
- debug('check content permissions');
97
-
98
- // @TODO: Remove when we drop v0.1
99
- // @TODO: https://github.com/TryGhost/Ghost/issues/10733
100
- return permissions.applyPublicRules(apiConfig.docName, apiConfig.method, {
101
- status: frame.options.status,
102
- id: frame.options.id,
103
- uuid: frame.options.uuid,
104
- slug: frame.options.slug,
105
- data: {
106
- status: frame.data.status,
107
- id: frame.data.id,
108
- uuid: frame.data.uuid,
109
- slug: frame.data.slug
110
- }
111
- });
96
+ debug('content api permissions pass-through');
97
+ return Promise.resolve(frame.options);
112
98
  }
113
99
 
114
100
  return nonePublicAuth(apiConfig, frame);
@@ -7,10 +7,10 @@ const localUtils = require('../../index');
7
7
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
8
8
  const clean = require('./utils/clean');
9
9
 
10
- function removeMobiledocFormat(frame) {
11
- if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
10
+ function removeSourceFormats(frame) {
11
+ if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
12
12
  frame.options.formats = frame.options.formats.filter((format) => {
13
- return (format !== 'mobiledoc');
13
+ return !['mobiledoc', 'lexical'].includes(format);
14
14
  });
15
15
  }
16
16
  }
@@ -95,7 +95,7 @@ module.exports = {
95
95
  forcePageFilter(frame);
96
96
 
97
97
  if (localUtils.isContentAPI(frame)) {
98
- removeMobiledocFormat(frame);
98
+ removeSourceFormats(frame);
99
99
  setDefaultOrder(frame);
100
100
  forceVisibilityColumn(frame);
101
101
  }
@@ -113,7 +113,7 @@ module.exports = {
113
113
  forcePageFilter(frame);
114
114
 
115
115
  if (localUtils.isContentAPI(frame)) {
116
- removeMobiledocFormat(frame);
116
+ removeSourceFormats(frame);
117
117
  setDefaultOrder(frame);
118
118
  forceVisibilityColumn(frame);
119
119
  }
@@ -6,11 +6,12 @@ const localUtils = require('../../index');
6
6
  const mobiledoc = require('../../../../../lib/mobiledoc');
7
7
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
8
8
  const clean = require('./utils/clean');
9
+ const labs = require('../../../../../../shared/labs');
9
10
 
10
- function removeMobiledocFormat(frame) {
11
- if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
11
+ function removeSourceFormats(frame) {
12
+ if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
12
13
  frame.options.formats = frame.options.formats.filter((format) => {
13
- return (format !== 'mobiledoc');
14
+ return !['mobiledoc', 'lexical'].includes(format);
14
15
  });
15
16
  }
16
17
  }
@@ -24,7 +25,11 @@ function defaultRelations(frame) {
24
25
  return false;
25
26
  }
26
27
 
27
- frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions'];
28
+ if (labs.isSet('emailClicks')) {
29
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions', 'count.clicks'];
30
+ } else {
31
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions'];
32
+ }
28
33
  }
29
34
 
30
35
  function setDefaultOrder(frame) {
@@ -101,8 +106,8 @@ module.exports = {
101
106
  * - user exists? admin api access
102
107
  */
103
108
  if (localUtils.isContentAPI(frame)) {
104
- // CASE: the content api endpoint for posts should not return mobiledoc
105
- removeMobiledocFormat(frame);
109
+ // CASE: the content api endpoint for posts should not return mobiledoc or lexical
110
+ removeSourceFormats(frame);
106
111
 
107
112
  setDefaultOrder(frame);
108
113
  forceVisibilityColumn(frame);
@@ -127,8 +132,8 @@ module.exports = {
127
132
  * - user exists? admin api access
128
133
  */
129
134
  if (localUtils.isContentAPI(frame)) {
130
- // CASE: the content api endpoint for posts should not return mobiledoc
131
- removeMobiledocFormat(frame);
135
+ // CASE: the content api endpoint for posts should not return mobiledoc or lexical
136
+ removeSourceFormats(frame);
132
137
 
133
138
  setDefaultOrder(frame);
134
139
  forceVisibilityColumn(frame);
@@ -47,6 +47,7 @@ const EDITABLE_SETTINGS = [
47
47
  'mailgun_domain',
48
48
  'mailgun_base_url',
49
49
  'email_track_opens',
50
+ 'email_track_clicks',
50
51
  'amp',
51
52
  'amp_gtag_id',
52
53
  'slack_url',
@@ -1,4 +1,6 @@
1
1
  const mapComment = require('./comments');
2
+ const url = require('../utils/url');
3
+ const _ = require('lodash');
2
4
 
3
5
  const commentEventMapper = (json, frame) => {
4
6
  return {
@@ -7,10 +9,59 @@ const commentEventMapper = (json, frame) => {
7
9
  };
8
10
  };
9
11
 
12
+ const clickEventMapper = (json, frame) => {
13
+ const memberFields = [
14
+ 'id',
15
+ 'uuid',
16
+ 'name',
17
+ 'email',
18
+ 'avatar_image'
19
+ ];
20
+
21
+ const linkFields = [
22
+ 'from',
23
+ 'to'
24
+ ];
25
+
26
+ const postFields = [
27
+ 'id',
28
+ 'uuid',
29
+ 'title',
30
+ 'url'
31
+ ];
32
+
33
+ const data = json.data;
34
+ const response = {};
35
+
36
+ if (data.link && data.link.post) {
37
+ // We could use the post mapper here, but we need less field + don't need al the async behavior support
38
+ url.forPost(data.link.post.id, data.link.post, frame);
39
+ response.post = _.pick(data.link.post, postFields);
40
+ }
41
+
42
+ if (data.link) {
43
+ response.link = _.pick(data.link, linkFields);
44
+ }
45
+
46
+ if (data.member) {
47
+ response.member = _.pick(data.member, memberFields);
48
+ } else {
49
+ response.member = null;
50
+ }
51
+
52
+ return {
53
+ ...json,
54
+ data: response
55
+ };
56
+ };
57
+
10
58
  const activityFeedMapper = (event, frame) => {
11
59
  if (event.type === 'comment_event') {
12
60
  return commentEventMapper(event, frame);
13
61
  }
62
+ if (event.type === 'click_event') {
63
+ return clickEventMapper(event, frame);
64
+ }
14
65
  return event;
15
66
  };
16
67
 
@@ -18,6 +18,15 @@ const memberFields = [
18
18
  'avatar_image'
19
19
  ];
20
20
 
21
+ const memberFieldsAdmin = [
22
+ 'id',
23
+ 'uuid',
24
+ 'name',
25
+ 'email',
26
+ 'expertise',
27
+ 'avatar_image'
28
+ ];
29
+
21
30
  const postFields = [
22
31
  'id',
23
32
  'uuid',
@@ -36,7 +45,7 @@ const commentMapper = (model, frame) => {
36
45
  const response = _.pick(jsonModel, commentFields);
37
46
 
38
47
  if (jsonModel.member) {
39
- response.member = _.pick(jsonModel.member, memberFields);
48
+ response.member = _.pick(jsonModel.member, utils.isMembersAPI(frame) ? memberFields : memberFieldsAdmin);
40
49
  } else {
41
50
  response.member = null;
42
51
  }
@@ -110,7 +110,7 @@ module.exports = async (model, frame, options = {}) => {
110
110
  });
111
111
  }
112
112
 
113
- if (!labs.isSet('memberAttribution')) {
113
+ if (!labs.isSet('memberAttribution') && !labs.isSet('emailClicks')) {
114
114
  delete jsonModel.count;
115
115
  }
116
116
 
@@ -4,7 +4,8 @@ const {ValidationError} = require('@tryghost/errors');
4
4
  const tpl = require('@tryghost/tpl');
5
5
 
6
6
  const messages = {
7
- invalidVisibilityFilter: 'Invalid filter in visibility_filter property'
7
+ invalidVisibilityFilter: 'Invalid filter in visibility_filter property',
8
+ onlySingleContentSource: 'It\'s only possible to save mobiledoc or lexical properties, not both'
8
9
  };
9
10
 
10
11
  const validateVisibility = async function (frame) {
@@ -33,15 +34,29 @@ const validateVisibility = async function (frame) {
33
34
  }
34
35
  };
35
36
 
37
+ const validateSingleContentSource = async function (frame) {
38
+ if (!frame.data.pages?.[0]) {
39
+ return;
40
+ }
41
+
42
+ const [page] = frame.data.pages;
43
+ if (page.mobiledoc && page.lexical) {
44
+ return Promise.reject(new ValidationError({
45
+ message: tpl(messages.onlySingleContentSource),
46
+ property: 'lexical'
47
+ }));
48
+ }
49
+ };
50
+
36
51
  module.exports = {
37
- add(apiConfig, frame) {
38
- return jsonSchema.validate(...arguments).then(() => {
39
- return validateVisibility(frame);
40
- });
52
+ async add(apiConfig, frame) {
53
+ await jsonSchema.validate(...arguments);
54
+ await validateVisibility(frame);
55
+ await validateSingleContentSource(frame);
41
56
  },
42
- edit(apiConfig, frame) {
43
- return jsonSchema.validate(...arguments).then(() => {
44
- return validateVisibility(frame);
45
- });
57
+ async edit(apiConfig, frame) {
58
+ await jsonSchema.validate(...arguments);
59
+ await validateVisibility(frame);
60
+ await validateSingleContentSource(frame);
46
61
  }
47
62
  };