ghost 5.16.2 → 5.17.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 (144) hide show
  1. package/components/tryghost-adapter-manager-5.17.0.tgz +0 -0
  2. package/components/tryghost-api-framework-5.17.0.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-5.17.0.tgz +0 -0
  4. package/components/tryghost-bootstrap-socket-5.17.0.tgz +0 -0
  5. package/components/tryghost-constants-5.17.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-5.17.0.tgz +0 -0
  7. package/components/tryghost-domain-events-5.17.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.17.0.tgz +0 -0
  9. package/components/{tryghost-email-analytics-service-5.16.2.tgz → tryghost-email-analytics-service-5.17.0.tgz} +0 -0
  10. package/components/tryghost-email-content-generator-5.17.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.17.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.17.0.tgz +0 -0
  13. package/components/tryghost-html-to-plaintext-5.17.0.tgz +0 -0
  14. package/components/tryghost-job-manager-5.17.0.tgz +0 -0
  15. package/components/tryghost-link-redirects-5.17.0.tgz +0 -0
  16. package/components/tryghost-link-replacer-5.17.0.tgz +0 -0
  17. package/components/tryghost-link-tracking-5.17.0.tgz +0 -0
  18. package/components/tryghost-magic-link-5.17.0.tgz +0 -0
  19. package/components/tryghost-mailgun-client-5.17.0.tgz +0 -0
  20. package/components/tryghost-member-analytics-service-5.17.0.tgz +0 -0
  21. package/components/tryghost-member-attribution-5.17.0.tgz +0 -0
  22. package/components/tryghost-member-events-5.17.0.tgz +0 -0
  23. package/components/tryghost-members-analytics-ingress-5.17.0.tgz +0 -0
  24. package/components/tryghost-members-api-5.17.0.tgz +0 -0
  25. package/components/tryghost-members-csv-5.17.0.tgz +0 -0
  26. package/components/tryghost-members-events-service-5.17.0.tgz +0 -0
  27. package/components/tryghost-members-importer-5.17.0.tgz +0 -0
  28. package/components/tryghost-members-offers-5.17.0.tgz +0 -0
  29. package/components/{tryghost-members-payments-5.16.2.tgz → tryghost-members-payments-5.17.0.tgz} +0 -0
  30. package/components/{tryghost-members-ssr-5.16.2.tgz → tryghost-members-ssr-5.17.0.tgz} +0 -0
  31. package/components/tryghost-members-stripe-service-5.17.0.tgz +0 -0
  32. package/components/tryghost-minifier-5.17.0.tgz +0 -0
  33. package/components/tryghost-mw-api-version-mismatch-5.17.0.tgz +0 -0
  34. package/components/tryghost-mw-cache-control-5.17.0.tgz +0 -0
  35. package/components/tryghost-mw-error-handler-5.17.0.tgz +0 -0
  36. package/components/tryghost-mw-session-from-token-5.17.0.tgz +0 -0
  37. package/components/tryghost-mw-update-user-last-seen-5.17.0.tgz +0 -0
  38. package/components/tryghost-mw-vhost-5.17.0.tgz +0 -0
  39. package/components/{tryghost-oembed-service-5.16.2.tgz → tryghost-oembed-service-5.17.0.tgz} +0 -0
  40. package/components/{tryghost-package-json-5.16.2.tgz → tryghost-package-json-5.17.0.tgz} +0 -0
  41. package/components/tryghost-referrers-5.17.0.tgz +0 -0
  42. package/components/{tryghost-security-5.16.2.tgz → tryghost-security-5.17.0.tgz} +0 -0
  43. package/components/tryghost-session-service-5.17.0.tgz +0 -0
  44. package/components/tryghost-settings-path-manager-5.17.0.tgz +0 -0
  45. package/components/tryghost-staff-service-5.17.0.tgz +0 -0
  46. package/components/tryghost-stats-service-5.17.0.tgz +0 -0
  47. package/components/tryghost-update-check-service-5.17.0.tgz +0 -0
  48. package/components/{tryghost-verification-trigger-5.16.2.tgz → tryghost-verification-trigger-5.17.0.tgz} +0 -0
  49. package/components/{tryghost-version-notifications-data-service-5.16.2.tgz → tryghost-version-notifications-data-service-5.17.0.tgz} +0 -0
  50. package/core/built/admin/assets/{chunk.143.4e48bf4e6c3d13258bb1.js → chunk.143.4ce1dcfd5c372638114d.js} +6 -6
  51. package/core/built/admin/assets/{chunk.174.e1e89637eab79fdd5c5d.js → chunk.174.37fefc669899f0fd0064.js} +153 -153
  52. package/core/built/admin/assets/{chunk.178.9c6e85adf674bd785cf1.js → chunk.178.824dedc0f9c31a8f64a7.js} +4 -4
  53. package/core/built/admin/assets/{chunk.579.d14c3688558f34afeb3e.js → chunk.579.a9bccec4d650a7be727a.js} +142 -88
  54. package/core/built/admin/assets/{chunk.579.d14c3688558f34afeb3e.js.LICENSE.txt → chunk.579.a9bccec4d650a7be727a.js.LICENSE.txt} +0 -0
  55. package/core/built/admin/assets/ghost-597fb8e8b1b91dd0ac4d9f2d75bd67fb.css +1 -0
  56. package/core/built/admin/assets/ghost-dark-e4ccecd9903d35d360d71fe859cbb3bf.css +1 -0
  57. package/core/built/admin/assets/{ghost-5064a384fad1061ca77a8604e479da90.js → ghost-fa7772f8aa2522b3935efed71fa75619.js} +390 -342
  58. package/core/built/admin/assets/vendor-325e038b8609f0979f6578cae7a87f9e.js +13270 -0
  59. package/core/built/admin/index.html +6 -6
  60. package/core/frontend/helpers/ghost_head.js +1 -1
  61. package/core/frontend/src/member-attribution/member-attribution.js +14 -14
  62. package/core/frontend/web/middleware/error-handler.js +3 -1
  63. package/core/frontend/web/site.js +9 -9
  64. package/core/server/api/endpoints/pages.js +1 -1
  65. package/core/server/api/endpoints/posts.js +1 -1
  66. package/core/server/api/endpoints/utils/serializers/input/pages.js +1 -1
  67. package/core/server/api/endpoints/utils/serializers/input/posts.js +2 -2
  68. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +23 -0
  69. package/core/server/api/endpoints/utils/serializers/output/members.js +20 -3
  70. package/core/server/api/endpoints/utils/validators/input/pages.js +5 -1
  71. package/core/server/api/endpoints/utils/validators/input/posts.js +5 -1
  72. package/core/server/data/exporter/table-lists.js +2 -2
  73. package/core/server/data/migrations/utils/tables.js +1 -1
  74. package/core/server/data/migrations/versions/5.16/2022-09-19-09-04-add-link-redirects-table.js +9 -10
  75. package/core/server/data/migrations/versions/5.16/2022-09-19-09-05-add-members-link-click-events-table.js +9 -8
  76. package/core/server/data/migrations/versions/5.17/2022-09-27-13-53-remove-click-tracking-tables.js +28 -0
  77. package/core/server/data/migrations/versions/5.17/2022-09-27-13-55-add-redirects-table.js +10 -0
  78. package/core/server/data/migrations/versions/5.17/2022-09-27-13-56-add-members-click-events-table.js +8 -0
  79. package/core/server/data/migrations/versions/5.17/2022-09-27-16-49-set-track-clicks-based-on-opens.js +31 -0
  80. package/core/server/data/migrations/versions/5.17/2022-09-29-12-39-add-track-clicks-column-to-emails.js +7 -0
  81. package/core/server/data/schema/schema.js +4 -3
  82. package/core/server/models/email.js +1 -0
  83. package/core/server/models/{member-link-click-event.js → member-click-event.js} +6 -6
  84. package/core/server/models/member.js +22 -0
  85. package/core/server/models/post.js +6 -6
  86. package/core/server/models/{link-redirect.js → redirect.js} +7 -7
  87. package/core/server/services/api-version-compatibility/index.js +6 -1
  88. package/core/server/services/link-redirection/index.js +1 -1
  89. package/core/server/services/link-tracking/LinkClickRepository.js +2 -2
  90. package/core/server/services/link-tracking/index.js +4 -4
  91. package/core/server/services/mega/mega.js +1 -0
  92. package/core/server/services/mega/post-email-serializer.js +17 -28
  93. package/core/server/services/member-attribution/index.js +2 -1
  94. package/core/server/services/members/api.js +1 -1
  95. package/core/server/web/api/endpoints/content/app.js +5 -2
  96. package/core/server/web/well-known.js +5 -1
  97. package/core/shared/config/defaults.json +14 -2
  98. package/core/shared/labs.js +2 -2
  99. package/package.json +106 -105
  100. package/yarn.lock +311 -299
  101. package/components/tryghost-adapter-manager-5.16.2.tgz +0 -0
  102. package/components/tryghost-api-framework-5.16.2.tgz +0 -0
  103. package/components/tryghost-api-version-compatibility-service-5.16.2.tgz +0 -0
  104. package/components/tryghost-bootstrap-socket-5.16.2.tgz +0 -0
  105. package/components/tryghost-constants-5.16.2.tgz +0 -0
  106. package/components/tryghost-custom-theme-settings-service-5.16.2.tgz +0 -0
  107. package/components/tryghost-domain-events-5.16.2.tgz +0 -0
  108. package/components/tryghost-email-analytics-provider-mailgun-5.16.2.tgz +0 -0
  109. package/components/tryghost-email-content-generator-5.16.2.tgz +0 -0
  110. package/components/tryghost-express-dynamic-redirects-5.16.2.tgz +0 -0
  111. package/components/tryghost-extract-api-key-5.16.2.tgz +0 -0
  112. package/components/tryghost-html-to-plaintext-5.16.2.tgz +0 -0
  113. package/components/tryghost-job-manager-5.16.2.tgz +0 -0
  114. package/components/tryghost-link-redirects-5.16.2.tgz +0 -0
  115. package/components/tryghost-link-replacer-5.16.2.tgz +0 -0
  116. package/components/tryghost-link-tracking-5.16.2.tgz +0 -0
  117. package/components/tryghost-magic-link-5.16.2.tgz +0 -0
  118. package/components/tryghost-mailgun-client-5.16.2.tgz +0 -0
  119. package/components/tryghost-member-analytics-service-5.16.2.tgz +0 -0
  120. package/components/tryghost-member-attribution-5.16.2.tgz +0 -0
  121. package/components/tryghost-member-events-5.16.2.tgz +0 -0
  122. package/components/tryghost-members-analytics-ingress-5.16.2.tgz +0 -0
  123. package/components/tryghost-members-api-5.16.2.tgz +0 -0
  124. package/components/tryghost-members-csv-5.16.2.tgz +0 -0
  125. package/components/tryghost-members-events-service-5.16.2.tgz +0 -0
  126. package/components/tryghost-members-importer-5.16.2.tgz +0 -0
  127. package/components/tryghost-members-offers-5.16.2.tgz +0 -0
  128. package/components/tryghost-members-stripe-service-5.16.2.tgz +0 -0
  129. package/components/tryghost-minifier-5.16.2.tgz +0 -0
  130. package/components/tryghost-mw-api-version-mismatch-5.16.2.tgz +0 -0
  131. package/components/tryghost-mw-cache-control-5.16.2.tgz +0 -0
  132. package/components/tryghost-mw-error-handler-5.16.2.tgz +0 -0
  133. package/components/tryghost-mw-session-from-token-5.16.2.tgz +0 -0
  134. package/components/tryghost-mw-update-user-last-seen-5.16.2.tgz +0 -0
  135. package/components/tryghost-mw-vhost-5.16.2.tgz +0 -0
  136. package/components/tryghost-referrers-5.16.2.tgz +0 -0
  137. package/components/tryghost-session-service-5.16.2.tgz +0 -0
  138. package/components/tryghost-settings-path-manager-5.16.2.tgz +0 -0
  139. package/components/tryghost-staff-service-5.16.2.tgz +0 -0
  140. package/components/tryghost-stats-service-5.16.2.tgz +0 -0
  141. package/components/tryghost-update-check-service-5.16.2.tgz +0 -0
  142. package/core/built/admin/assets/ghost-6491d134c450ca676911ea17e16cd7d4.css +0 -1
  143. package/core/built/admin/assets/ghost-dark-297ab2fcf4cadd1c950b84089a38c5e2.css +0 -1
  144. package/core/built/admin/assets/vendor-b2375e2f383cbc3fd73340c4b656c993.js +0 -13654
@@ -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.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" />
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.17%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%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -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-6491d134c450ca676911ea17e16cd7d4.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-597fb8e8b1b91dd0ac4d9f2d75bd67fb.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-b2375e2f383cbc3fd73340c4b656c993.js"></script>
60
- <script src="assets/chunk.579.d14c3688558f34afeb3e.js"></script>
61
- <script src="assets/chunk.143.4e48bf4e6c3d13258bb1.js"></script>
62
- <script src="assets/ghost-5064a384fad1061ca77a8604e479da90.js"></script>
59
+ <script src="assets/vendor-325e038b8609f0979f6578cae7a87f9e.js"></script>
60
+ <script src="assets/chunk.579.a9bccec4d650a7be727a.js"></script>
61
+ <script src="assets/chunk.143.4ce1dcfd5c372638114d.js"></script>
62
+ <script src="assets/ghost-fa7772f8aa2522b3935efed71fa75619.js"></script>
63
63
  </body>
64
64
  </html>
@@ -234,7 +234,7 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
234
234
  head.push(`<script defer src="${getAssetUrl('public/comment-counts.min.js')}" data-ghost-comments-counts-api="${urlUtils.getSiteUrl(true)}members/api/comments/counts/"></script>`);
235
235
  }
236
236
 
237
- if (labs.isSet('memberAttribution')) {
237
+ if (settingsCache.get('members_enabled')) {
238
238
  head.push(`<script defer src="${getAssetUrl('public/member-attribution.min.js')}"></script>`);
239
239
  }
240
240
 
@@ -87,9 +87,9 @@ const LIMIT = 15;
87
87
  console.error('[Member Attribution] Parsing referrer from querystring failed', e);
88
88
  }
89
89
 
90
- const refSource = refParam || sourceParam || utmSourceParam || null;
91
- const refMedium = utmMediumParam || null;
92
- const refUrl = window.document.referrer || null;
90
+ const referrerSource = refParam || sourceParam || utmSourceParam || null;
91
+ const referrerMedium = utmMediumParam || null;
92
+ const referrerUrl = window.document.referrer || null;
93
93
 
94
94
  // Do we have attributions in the query string?
95
95
  try {
@@ -101,9 +101,9 @@ const LIMIT = 15;
101
101
  time: currentTime,
102
102
  id: params.get('attribution_id'),
103
103
  type: params.get('attribution_type'),
104
- refSource,
105
- refMedium,
106
- refUrl
104
+ referrerSource,
105
+ referrerMedium,
106
+ referrerUrl
107
107
  });
108
108
 
109
109
  // Remove attribution from query string
@@ -122,19 +122,19 @@ const LIMIT = 15;
122
122
  history.push({
123
123
  path: currentPath,
124
124
  time: currentTime,
125
- refSource,
126
- refMedium,
127
- refUrl
125
+ referrerSource,
126
+ referrerMedium,
127
+ referrerUrl
128
128
  });
129
129
  } else if (history.length > 0) {
130
130
  history[history.length - 1].time = currentTime;
131
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;
132
+ if (referrerSource) {
133
+ history[history.length - 1].referrerSource = referrerSource;
134
+ history[history.length - 1].referrerMedium = referrerMedium;
135
135
  }
136
- if (refUrl) {
137
- history[history.length - 1].refUrl = refUrl;
136
+ if (referrerUrl) {
137
+ history[history.length - 1].referrerUrl = referrerUrl;
138
138
  }
139
139
  }
140
140
 
@@ -7,7 +7,7 @@ const config = require('../../../shared/config');
7
7
  const renderer = require('../../services/rendering');
8
8
 
9
9
  // @TODO: make this properly shared code
10
- const {prepareError, prepareStack} = require('@tryghost/mw-error-handler');
10
+ const {prepareError, prepareErrorCacheControl, prepareStack} = require('@tryghost/mw-error-handler');
11
11
 
12
12
  const messages = {
13
13
  oopsErrorTemplateHasError: 'Oops, seems there is an error in the error template.',
@@ -86,6 +86,8 @@ const themeErrorRenderer = (err, req, res, next) => {
86
86
  module.exports.handleThemeResponse = [
87
87
  // Make sure the error can be served
88
88
  prepareError,
89
+ // Add cache-control header
90
+ prepareErrorCacheControl(),
89
91
  // Handle the error in Sentry
90
92
  sentry.errorHandler,
91
93
  // Format the stack for the user
@@ -65,21 +65,21 @@ module.exports = function setupSiteApp(routerConfig) {
65
65
  siteApp.use(mw.serveFavicon());
66
66
 
67
67
  // Serve sitemap.xsl file
68
- siteApp.use(mw.servePublicFile('static', 'sitemap.xsl', 'text/xsl', constants.ONE_DAY_S));
68
+ siteApp.use(mw.servePublicFile('static', 'sitemap.xsl', 'text/xsl', config.get('caching:sitemapXSL:maxAge')));
69
69
 
70
70
  // Serve stylesheets for default templates
71
- siteApp.use(mw.servePublicFile('static', 'public/ghost.css', 'text/css', constants.ONE_HOUR_S));
72
- siteApp.use(mw.servePublicFile('static', 'public/ghost.min.css', 'text/css', constants.ONE_YEAR_S));
71
+ siteApp.use(mw.servePublicFile('static', 'public/ghost.css', 'text/css', config.get('caching:publicAssets:maxAge')));
72
+ siteApp.use(mw.servePublicFile('static', 'public/ghost.min.css', 'text/css', config.get('caching:publicAssets:maxAge')));
73
73
 
74
74
  // Card assets
75
- siteApp.use(mw.servePublicFile('built', 'public/cards.min.css', 'text/css', constants.ONE_YEAR_S));
76
- siteApp.use(mw.servePublicFile('built', 'public/cards.min.js', 'application/javascript', constants.ONE_YEAR_S));
75
+ siteApp.use(mw.servePublicFile('built', 'public/cards.min.css', 'text/css', config.get('caching:publicAssets:maxAge')));
76
+ siteApp.use(mw.servePublicFile('built', 'public/cards.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));
77
77
 
78
78
  // Comment counts
79
- siteApp.use(mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', constants.ONE_YEAR_S));
79
+ siteApp.use(mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));
80
80
 
81
81
  // Member attribution
82
- siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', constants.ONE_YEAR_S));
82
+ siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', config.get('caching:publicAssets:maxAge')));
83
83
 
84
84
  // Serve site images using the storage adapter
85
85
  siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
@@ -94,7 +94,7 @@ module.exports = function setupSiteApp(routerConfig) {
94
94
  // /member/.well-known/* serves files (e.g. jwks.json) so it needs to be mounted before the prettyUrl mw to avoid trailing slashes
95
95
  siteApp.use(
96
96
  '/members/.well-known',
97
- shared.middleware.cacheControl('public', {maxAge: constants.ONE_DAY_S}),
97
+ shared.middleware.cacheControl('public', {maxAge: config.get('caching:wellKnown:maxAge')}),
98
98
  (req, res, next) => membersService.api.middleware.wellKnown(req, res, next)
99
99
  );
100
100
 
@@ -118,7 +118,7 @@ module.exports = function setupSiteApp(routerConfig) {
118
118
  debug('Themes done');
119
119
 
120
120
  // Serve robots.txt if not found in theme
121
- siteApp.use(mw.servePublicFile('static', 'robots.txt', 'text/plain', constants.ONE_HOUR_S));
121
+ siteApp.use(mw.servePublicFile('static', 'robots.txt', 'text/plain', config.get('caching:robotstxt:maxAge')));
122
122
 
123
123
  // site map - this should probably be refactored to be an internal app
124
124
  sitemapHandler(siteApp);
@@ -2,7 +2,7 @@ const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const getPostServiceInstance = require('../../services/posts/posts-service');
5
- const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles', 'tiers', 'count.signups', 'count.conversions'];
5
+ const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles', 'tiers', 'count.signups', 'count.paid_conversions'];
6
6
  const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
7
7
 
8
8
  const messages = {
@@ -10,7 +10,7 @@ const allowedIncludes = [
10
10
  'tiers',
11
11
  'newsletter',
12
12
  'count.signups',
13
- 'count.conversions',
13
+ 'count.paid_conversions',
14
14
  'count.clicks'
15
15
  ];
16
16
  const unsafeAttrs = ['status', 'authors', 'visibility'];
@@ -24,7 +24,7 @@ function defaultRelations(frame) {
24
24
  return false;
25
25
  }
26
26
 
27
- frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'tiers', 'count.signups', 'count.conversions'];
27
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'tiers', 'count.signups', 'count.paid_conversions'];
28
28
  }
29
29
 
30
30
  function setDefaultOrder(frame) {
@@ -26,9 +26,9 @@ function defaultRelations(frame) {
26
26
  }
27
27
 
28
28
  if (labs.isSet('emailClicks')) {
29
- frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions', 'count.clicks'];
29
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.paid_conversions', 'count.clicks'];
30
30
  } else {
31
- frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions'];
31
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.paid_conversions'];
32
32
  }
33
33
  }
34
34
 
@@ -49,12 +49,32 @@ const clickEventMapper = (json, frame) => {
49
49
  response.member = null;
50
50
  }
51
51
 
52
+ if (data.created_at) {
53
+ response.created_at = data.created_at;
54
+ }
55
+
52
56
  return {
53
57
  ...json,
54
58
  data: response
55
59
  };
56
60
  };
57
61
 
62
+ function serializeAttribution(attribution) {
63
+ if (!attribution) {
64
+ return attribution;
65
+ }
66
+
67
+ return {
68
+ id: attribution?.id,
69
+ type: attribution?.type,
70
+ url: attribution?.url,
71
+ title: attribution?.title,
72
+ referrer_source: attribution?.referrerSource,
73
+ referrer_medium: attribution?.referrerMedium,
74
+ referrer_url: attribution.referrerUrl
75
+ };
76
+ }
77
+
58
78
  const activityFeedMapper = (event, frame) => {
59
79
  if (event.type === 'comment_event') {
60
80
  return commentEventMapper(event, frame);
@@ -62,6 +82,9 @@ const activityFeedMapper = (event, frame) => {
62
82
  if (event.type === 'click_event') {
63
83
  return clickEventMapper(event, frame);
64
84
  }
85
+ if (event.data?.attribution) {
86
+ event.data.attribution = serializeAttribution(event.data.attribution);
87
+ }
65
88
  return event;
66
89
  };
67
90
 
@@ -96,6 +96,22 @@ function exportCSV(data) {
96
96
  return unparse(data.data);
97
97
  }
98
98
 
99
+ function serializeAttribution(attribution) {
100
+ if (!attribution) {
101
+ return attribution;
102
+ }
103
+
104
+ return {
105
+ id: attribution?.id,
106
+ type: attribution?.type,
107
+ url: attribution?.url,
108
+ title: attribution?.title,
109
+ referrer_source: attribution?.referrerSource,
110
+ referrer_medium: attribution?.referrerMedium,
111
+ referrer_url: attribution.referrerUrl
112
+ };
113
+ }
114
+
99
115
  /**
100
116
  * @param {import('bookshelf').Model} member
101
117
  * @param {object} options
@@ -129,7 +145,7 @@ function serializeMember(member, options) {
129
145
  email_recipients: json.email_recipients,
130
146
  status: json.status,
131
147
  last_seen_at: json.last_seen_at,
132
- attribution: json.attribution
148
+ attribution: serializeAttribution(json.attribution)
133
149
  };
134
150
 
135
151
  if (json.products) {
@@ -141,15 +157,16 @@ function serializeMember(member, options) {
141
157
  if (!subscription.price) {
142
158
  continue;
143
159
  }
144
-
160
+
145
161
  if (!subscription.price.tier && subscription.price.product) {
146
162
  subscription.price.tier = subscription.price.product;
147
-
163
+
148
164
  if (!subscription.price.tier.tier_id) {
149
165
  subscription.price.tier.tier_id = subscription.price.tier.product_id;
150
166
  }
151
167
  delete subscription.price.tier.product_id;
152
168
  }
169
+ subscription.attribution = serializeAttribution(subscription.attribution);
153
170
  delete subscription.price.product;
154
171
  }
155
172
 
@@ -5,7 +5,9 @@ const tpl = require('@tryghost/tpl');
5
5
 
6
6
  const messages = {
7
7
  invalidVisibilityFilter: 'Invalid filter in visibility_filter property',
8
- onlySingleContentSource: 'It\'s only possible to save mobiledoc or lexical properties, not both'
8
+ onlySingleContentSource: 'Pages can have either a mobiledoc or a lexical property, never both.',
9
+ onlySingleContentSourceContext: 'Both the mobiledoc and lexical properties are set, one must be null',
10
+ onlySingleContentSourceHelp: 'https://ghost.org/docs/admin-api/#the-post-object'
9
11
  };
10
12
 
11
13
  const validateVisibility = async function (frame) {
@@ -43,6 +45,8 @@ const validateSingleContentSource = async function (frame) {
43
45
  if (page.mobiledoc && page.lexical) {
44
46
  return Promise.reject(new ValidationError({
45
47
  message: tpl(messages.onlySingleContentSource),
48
+ context: tpl(messages.onlySingleContentSourceContext),
49
+ help: tpl(messages.onlySingleContentSourceHelp),
46
50
  property: 'lexical'
47
51
  }));
48
52
  }
@@ -5,7 +5,9 @@ const tpl = require('@tryghost/tpl');
5
5
 
6
6
  const messages = {
7
7
  invalidVisibilityFilter: 'Invalid filter in visibility_filter property',
8
- onlySingleContentSource: 'It\'s only possible to save mobiledoc or lexical properties, not both'
8
+ onlySingleContentSource: 'Posts can have either a mobiledoc or a lexical property, never both.',
9
+ onlySingleContentSourceContext: 'Both the mobiledoc and lexical properties are set, one must be null',
10
+ onlySingleContentSourceHelp: 'https://ghost.org/docs/admin-api/#the-post-object'
9
11
  };
10
12
 
11
13
  const validateVisibility = async function (frame) {
@@ -43,6 +45,8 @@ const validateSingleContentSource = async function (frame) {
43
45
  if (post.mobiledoc && post.lexical) {
44
46
  return Promise.reject(new ValidationError({
45
47
  message: tpl(messages.onlySingleContentSource),
48
+ context: tpl(messages.onlySingleContentSourceContext),
49
+ help: tpl(messages.onlySingleContentSourceHelp),
46
50
  property: 'lexical'
47
51
  }));
48
52
  }
@@ -39,8 +39,8 @@ const BACKUP_TABLES = [
39
39
  'comment_likes',
40
40
  'comment_reports',
41
41
  'jobs',
42
- 'link_redirects',
43
- 'members_link_click_events'
42
+ 'redirects',
43
+ 'members_click_events'
44
44
  ];
45
45
 
46
46
  // NOTE: exposing only tables which are going to be included in a "default" export file
@@ -38,7 +38,7 @@ function addTable(name, tableSpec) {
38
38
  /**
39
39
  * Creates migration which will drop a table
40
40
  *
41
- * @param {[string]} names - names of the tables to drop
41
+ * @param {string[]} names - names of the tables to drop
42
42
  */
43
43
  function dropTables(names) {
44
44
  return createIrreversibleMigration(
@@ -1,10 +1,9 @@
1
- const {addTable} = require('../../utils');
2
-
3
- module.exports = addTable('link_redirects', {
4
- id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
- from: {type: 'string', maxlength: 2000, nullable: false},
6
- to: {type: 'string', maxlength: 2000, nullable: false},
7
- post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
8
- created_at: {type: 'dateTime', nullable: false},
9
- updated_at: {type: 'dateTime', nullable: true}
10
- });
1
+ const logging = require('@tryghost/logging');
2
+ module.exports = {
3
+ async up() {
4
+ logging.warn('Skipping migration - noop');
5
+ },
6
+ async down() {
7
+ logging.warn('Skipping migration - noop');
8
+ }
9
+ };
@@ -1,8 +1,9 @@
1
- const {addTable} = require('../../utils');
2
-
3
- module.exports = addTable('members_link_click_events', {
4
- id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
- member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
6
- link_id: {type: 'string', maxlength: 24, nullable: false, references: 'link_redirects.id', cascadeDelete: true},
7
- created_at: {type: 'dateTime', nullable: false}
8
- });
1
+ const logging = require('@tryghost/logging');
2
+ module.exports = {
3
+ async up() {
4
+ logging.warn('Skipping migration - noop');
5
+ },
6
+ async down() {
7
+ logging.warn('Skipping migration - noop');
8
+ }
9
+ };
@@ -0,0 +1,28 @@
1
+ const {addTable, combineNonTransactionalMigrations} = require('../../utils');
2
+
3
+ function reverseMigration({up, down, config}) {
4
+ return {
5
+ config,
6
+ up: down,
7
+ down: up
8
+ };
9
+ }
10
+
11
+ module.exports = reverseMigration(
12
+ combineNonTransactionalMigrations(
13
+ addTable('link_redirects', {
14
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
15
+ from: {type: 'string', maxlength: 2000, nullable: false},
16
+ to: {type: 'string', maxlength: 2000, nullable: false},
17
+ post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
18
+ created_at: {type: 'dateTime', nullable: false},
19
+ updated_at: {type: 'dateTime', nullable: true}
20
+ }),
21
+ addTable('members_link_click_events', {
22
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
23
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
24
+ link_id: {type: 'string', maxlength: 24, nullable: false, references: 'link_redirects.id', cascadeDelete: true},
25
+ created_at: {type: 'dateTime', nullable: false}
26
+ })
27
+ )
28
+ );
@@ -0,0 +1,10 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('redirects', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ from: {type: 'string', maxlength: 2000, nullable: false},
6
+ to: {type: 'string', maxlength: 2000, nullable: false},
7
+ post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
8
+ created_at: {type: 'dateTime', nullable: false},
9
+ updated_at: {type: 'dateTime', nullable: true}
10
+ });
@@ -0,0 +1,8 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('members_click_events', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
6
+ redirect_id: {type: 'string', maxlength: 24, nullable: false, references: 'redirects.id', cascadeDelete: true},
7
+ created_at: {type: 'dateTime', nullable: false}
8
+ });
@@ -0,0 +1,31 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ // Set email_track_clicks to the current value of email_track_opens
5
+ module.exports = createTransactionalMigration(
6
+ async function up(connection) {
7
+ const reuseValueOfSetting = await connection('settings')
8
+ .where('key', '=', 'email_track_opens')
9
+ .first();
10
+
11
+ if (!reuseValueOfSetting) {
12
+ logging.warn(`Skipped setting email_track_clicks to current value of email_track_opens - email_track_opens not found`);
13
+ return;
14
+ }
15
+
16
+ const affectedRows = await connection('settings')
17
+ .update({
18
+ value: reuseValueOfSetting.value
19
+ })
20
+ .where('key', '=', 'email_track_clicks');
21
+
22
+ if (affectedRows === 1) {
23
+ logging.info(`Set email_track_clicks to ${reuseValueOfSetting.value} (current email_track_opens value)`);
24
+ } else {
25
+ logging.warn(`Tried setting email_track_clicks to ${reuseValueOfSetting.value} — ${affectedRows} changes`);
26
+ }
27
+ },
28
+ async function down() {
29
+ // no-op: we don't need to change it back
30
+ }
31
+ );
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('emails', 'track_clicks', {
4
+ type: 'bool',
5
+ nullable: false,
6
+ defaultTo: false
7
+ });
@@ -723,6 +723,7 @@ module.exports = {
723
723
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
724
724
  plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
725
725
  track_opens: {type: 'bool', nullable: false, defaultTo: false},
726
+ track_clicks: {type: 'bool', nullable: false, defaultTo: false},
726
727
  submitted_at: {type: 'dateTime', nullable: false},
727
728
  newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
728
729
  created_at: {type: 'dateTime', nullable: false},
@@ -836,7 +837,7 @@ module.exports = {
836
837
  created_at: {type: 'dateTime', nullable: false},
837
838
  updated_at: {type: 'dateTime', nullable: true}
838
839
  },
839
- link_redirects: {
840
+ redirects: {
840
841
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
841
842
  from: {type: 'string', maxlength: 2000, nullable: false},
842
843
  to: {type: 'string', maxlength: 2000, nullable: false},
@@ -844,10 +845,10 @@ module.exports = {
844
845
  created_at: {type: 'dateTime', nullable: false},
845
846
  updated_at: {type: 'dateTime', nullable: true}
846
847
  },
847
- members_link_click_events: {
848
+ members_click_events: {
848
849
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
849
850
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
850
- link_id: {type: 'string', maxlength: 24, nullable: false, references: 'link_redirects.id', cascadeDelete: true},
851
+ redirect_id: {type: 'string', maxlength: 24, nullable: false, references: 'redirects.id', cascadeDelete: true},
851
852
  created_at: {type: 'dateTime', nullable: false}
852
853
  }
853
854
  };
@@ -10,6 +10,7 @@ const Email = ghostBookshelf.Model.extend({
10
10
  status: 'pending',
11
11
  recipient_filter: 'status:-free',
12
12
  track_opens: false,
13
+ track_clicks: false,
13
14
  delivered_count: 0,
14
15
  opened_count: 0,
15
16
  failed_count: 0
@@ -1,11 +1,11 @@
1
1
  const errors = require('@tryghost/errors');
2
2
  const ghostBookshelf = require('./base');
3
3
 
4
- const MemberLinkClickEvent = ghostBookshelf.Model.extend({
5
- tableName: 'members_link_click_events',
4
+ const MemberClickEvent = ghostBookshelf.Model.extend({
5
+ tableName: 'members_click_events',
6
6
 
7
7
  link() {
8
- return this.belongsTo('LinkRedirect', 'link_id');
8
+ return this.belongsTo('Redirect', 'link_id');
9
9
  },
10
10
 
11
11
  member() {
@@ -13,14 +13,14 @@ const MemberLinkClickEvent = ghostBookshelf.Model.extend({
13
13
  }
14
14
  }, {
15
15
  async edit() {
16
- throw new errors.IncorrectUsageError({message: 'Cannot edit MemberLinkClickEvent'});
16
+ throw new errors.IncorrectUsageError({message: 'Cannot edit MemberClickEvent'});
17
17
  },
18
18
 
19
19
  async destroy() {
20
- throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberLinkClickEvent'});
20
+ throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberClickEvent'});
21
21
  }
22
22
  });
23
23
 
24
24
  module.exports = {
25
- MemberLinkClickEvent: ghostBookshelf.model('MemberLinkClickEvent', MemberLinkClickEvent)
25
+ MemberClickEvent: ghostBookshelf.model('MemberClickEvent', MemberClickEvent)
26
26
  };
@@ -46,6 +46,12 @@ const Member = ghostBookshelf.Model.extend({
46
46
  }, {
47
47
  key: 'conversion',
48
48
  replacement: 'conversions.attribution_id'
49
+ }, {
50
+ key: 'opened_emails.post_id',
51
+ replacement: 'emails.post_id',
52
+ // Currently we cannot expand on values such as null or a string in mongo-knex
53
+ // But the line below is essentially the same as: `email_recipients.opened_at:-null`
54
+ expansion: 'email_recipients.opened_at:>=0'
49
55
  }];
50
56
  },
51
57
 
@@ -92,6 +98,22 @@ const Member = ghostBookshelf.Model.extend({
92
98
  tableNameAs: 'conversions',
93
99
  type: 'oneToOne',
94
100
  joinFrom: 'member_id'
101
+ },
102
+ clicked_links: {
103
+ tableName: 'link_redirects',
104
+ tableNameAs: 'clicked_links',
105
+ type: 'manyToMany',
106
+ joinTable: 'members_link_click_events',
107
+ joinFrom: 'member_id',
108
+ joinTo: 'link_id'
109
+ },
110
+ emails: {
111
+ tableName: 'emails',
112
+ tableNameAs: 'emails',
113
+ type: 'manyToMany',
114
+ joinTable: 'email_recipients',
115
+ joinFrom: 'member_id',
116
+ joinTo: 'email_id'
95
117
  }
96
118
  };
97
119
  },
@@ -1338,20 +1338,20 @@ Post = ghostBookshelf.Model.extend({
1338
1338
  .as('count__signups');
1339
1339
  });
1340
1340
  },
1341
- conversions(modelOrCollection) {
1341
+ paid_conversions(modelOrCollection) {
1342
1342
  modelOrCollection.query('columns', 'posts.*', (qb) => {
1343
1343
  qb.count('members_subscription_created_events.id')
1344
1344
  .from('members_subscription_created_events')
1345
1345
  .whereRaw('posts.id = members_subscription_created_events.attribution_id')
1346
- .as('count__conversions');
1346
+ .as('count__paid_conversions');
1347
1347
  });
1348
1348
  },
1349
1349
  clicks(modelOrCollection) {
1350
1350
  modelOrCollection.query('columns', 'posts.*', (qb) => {
1351
- qb.countDistinct('members_link_click_events.member_id')
1352
- .from('members_link_click_events')
1353
- .join('link_redirects', 'members_link_click_events.link_id', 'link_redirects.id')
1354
- .whereRaw('posts.id = link_redirects.post_id')
1351
+ qb.countDistinct('members_click_events.member_id')
1352
+ .from('members_click_events')
1353
+ .join('redirects', 'members_click_events.redirect_id', 'redirects.id')
1354
+ .whereRaw('posts.id = redirects.post_id')
1355
1355
  .as('count__clicks');
1356
1356
  });
1357
1357
  }