ghost 5.14.2 → 5.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/components/{tryghost-adapter-manager-5.14.2.tgz → tryghost-adapter-manager-5.15.0.tgz} +0 -0
  2. package/components/tryghost-api-framework-5.15.0.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-5.15.0.tgz +0 -0
  4. package/components/tryghost-bootstrap-socket-5.15.0.tgz +0 -0
  5. package/components/tryghost-constants-5.15.0.tgz +0 -0
  6. package/components/{tryghost-custom-theme-settings-service-5.14.2.tgz → tryghost-custom-theme-settings-service-5.15.0.tgz} +0 -0
  7. package/components/tryghost-domain-events-5.15.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.15.0.tgz +0 -0
  9. package/components/{tryghost-email-analytics-service-5.14.2.tgz → tryghost-email-analytics-service-5.15.0.tgz} +0 -0
  10. package/components/tryghost-email-content-generator-5.15.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.15.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.15.0.tgz +0 -0
  13. package/components/tryghost-html-to-plaintext-5.15.0.tgz +0 -0
  14. package/components/{tryghost-job-manager-5.14.2.tgz → tryghost-job-manager-5.15.0.tgz} +0 -0
  15. package/components/tryghost-link-redirects-5.15.0.tgz +0 -0
  16. package/components/tryghost-link-replacement-5.15.0.tgz +0 -0
  17. package/components/tryghost-link-tracking-5.15.0.tgz +0 -0
  18. package/components/{tryghost-magic-link-5.14.2.tgz → tryghost-magic-link-5.15.0.tgz} +0 -0
  19. package/components/tryghost-mailgun-client-5.15.0.tgz +0 -0
  20. package/components/tryghost-member-analytics-service-5.15.0.tgz +0 -0
  21. package/components/tryghost-member-attribution-5.15.0.tgz +0 -0
  22. package/components/tryghost-member-events-5.15.0.tgz +0 -0
  23. package/components/tryghost-members-analytics-ingress-5.15.0.tgz +0 -0
  24. package/components/tryghost-members-api-5.15.0.tgz +0 -0
  25. package/components/tryghost-members-csv-5.15.0.tgz +0 -0
  26. package/components/tryghost-members-events-service-5.15.0.tgz +0 -0
  27. package/components/tryghost-members-importer-5.15.0.tgz +0 -0
  28. package/components/tryghost-members-offers-5.15.0.tgz +0 -0
  29. package/components/{tryghost-members-payments-5.14.2.tgz → tryghost-members-payments-5.15.0.tgz} +0 -0
  30. package/components/{tryghost-members-ssr-5.14.2.tgz → tryghost-members-ssr-5.15.0.tgz} +0 -0
  31. package/components/tryghost-members-stripe-service-5.15.0.tgz +0 -0
  32. package/components/tryghost-minifier-5.15.0.tgz +0 -0
  33. package/components/tryghost-mw-api-version-mismatch-5.15.0.tgz +0 -0
  34. package/components/tryghost-mw-cache-control-5.15.0.tgz +0 -0
  35. package/components/{tryghost-mw-error-handler-5.14.2.tgz → tryghost-mw-error-handler-5.15.0.tgz} +0 -0
  36. package/components/tryghost-mw-session-from-token-5.15.0.tgz +0 -0
  37. package/components/tryghost-mw-update-user-last-seen-5.15.0.tgz +0 -0
  38. package/components/tryghost-mw-vhost-5.15.0.tgz +0 -0
  39. package/components/{tryghost-oembed-service-5.14.2.tgz → tryghost-oembed-service-5.15.0.tgz} +0 -0
  40. package/components/{tryghost-package-json-5.14.2.tgz → tryghost-package-json-5.15.0.tgz} +0 -0
  41. package/components/tryghost-security-5.15.0.tgz +0 -0
  42. package/components/{tryghost-session-service-5.14.2.tgz → tryghost-session-service-5.15.0.tgz} +0 -0
  43. package/components/{tryghost-settings-path-manager-5.14.2.tgz → tryghost-settings-path-manager-5.15.0.tgz} +0 -0
  44. package/components/tryghost-staff-service-5.15.0.tgz +0 -0
  45. package/components/{tryghost-update-check-service-5.14.2.tgz → tryghost-update-check-service-5.15.0.tgz} +0 -0
  46. package/components/{tryghost-verification-trigger-5.14.2.tgz → tryghost-verification-trigger-5.15.0.tgz} +0 -0
  47. package/components/{tryghost-version-notifications-data-service-5.14.2.tgz → tryghost-version-notifications-data-service-5.15.0.tgz} +0 -0
  48. package/content/themes/casper/default.hbs +2 -2
  49. package/core/boot.js +12 -3
  50. package/core/built/admin/assets/{chunk.143.a5ef705453da0d58b75a.js → chunk.143.558b9943af7b15f189ae.js} +20 -20
  51. package/core/built/admin/assets/{chunk.174.2edaa0869bfc2d88cf90.js → chunk.174.e1e89637eab79fdd5c5d.js} +68 -68
  52. package/core/built/admin/assets/{chunk.178.579a6edabc75a2d7378f.js → chunk.178.a9f6ddaea01e2bc76235.js} +4 -4
  53. package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js → chunk.579.dc11bf8dda5cf4406708.js} +5464 -4961
  54. package/core/built/admin/assets/{chunk.579.2de3f4300baf25f9a0db.js.LICENSE.txt → chunk.579.dc11bf8dda5cf4406708.js.LICENSE.txt} +0 -0
  55. package/core/built/admin/assets/fonts/{Inter.ttf → Inter-e19174fb2c0e19b1fa67492a07886c75.ttf} +0 -0
  56. package/core/built/admin/assets/{ghost-8919656440ad4617a07bb31069b1f71b.js → ghost-4b1b550e34300f5f4774a261aac29557.js} +487 -470
  57. package/core/built/admin/assets/ghost-c933adafb359b75ea1577365ce252e76.css +1 -0
  58. package/core/built/admin/assets/ghost-dark-04981c84bf590e0fae0a8e83e018190f.css +1 -0
  59. package/core/built/admin/assets/img/{amp.svg → amp-d7b72aae3315fda95921fb575dfca100.svg} +0 -0
  60. package/core/built/admin/assets/img/{disqus.svg → disqus-43503a3fa4f38dc8c61c7358b811f343.svg} +0 -0
  61. package/core/built/admin/assets/img/{favicon.ico → favicon-a9c6dbdcdc3ae568f4e0dad92149a0e3.ico} +0 -0
  62. package/core/built/admin/assets/img/{github.svg → github-c3a739c59df26fed12c10ffb00b33bd4.svg} +0 -0
  63. package/core/built/admin/assets/img/{google-docs.svg → google-docs-1e42cc272fc088da49e4b0ddfb01b006.svg} +0 -0
  64. package/core/built/admin/assets/img/{mailchimp.svg → mailchimp-f22b1e130aac764965b9306d7265a6b2.svg} +0 -0
  65. package/core/built/admin/assets/img/{patreon.svg → patreon-b19a5e6418a72977a16b30039d374d04.svg} +0 -0
  66. package/core/built/admin/assets/img/{paypal.svg → paypal-38e9448ce7549ea4caf8e7753ae661d6.svg} +0 -0
  67. package/core/built/admin/assets/img/{twitter.svg → twitter-7a7a0ba12d9b5bfb8a2058764a827c31.svg} +0 -0
  68. package/core/built/admin/assets/img/{typeform.svg → typeform-9f23f8712d776a7515594676285266f5.svg} +0 -0
  69. package/core/built/admin/assets/img/{unsplash.svg → unsplash-5b329eef0b11447b4117eaf817ebad6f.svg} +0 -0
  70. package/core/built/admin/assets/img/{zapier.svg → zapier-bf93bc440a3fd43b73489a63c215cdc7.svg} +0 -0
  71. package/core/built/admin/assets/img/{zapier-logo.svg → zapier-logo-a125f24313dfe01ef49af01fc90061fb.svg} +0 -0
  72. package/core/built/admin/assets/{vendor-eb76d0236a09b8b6f44675dba45b1fc6.js → vendor-271c32988ab16ba175a9bfa2acb2887a.js} +45 -39
  73. package/core/built/admin/assets/videos/logo-loader.mp4 +0 -0
  74. package/core/built/admin/index.html +11 -8
  75. package/core/frontend/src/member-attribution/member-attribution.js +27 -0
  76. package/core/frontend/web/site.js +10 -7
  77. package/core/server/api/endpoints/redirects.js +6 -8
  78. package/core/server/api/endpoints/utils/permissions.js +2 -16
  79. package/core/server/api/endpoints/utils/serializers/input/pages.js +5 -5
  80. package/core/server/api/endpoints/utils/serializers/input/posts.js +7 -7
  81. package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
  82. package/core/server/api/endpoints/utils/validators/input/pages.js +24 -9
  83. package/core/server/api/endpoints/utils/validators/input/posts.js +24 -9
  84. package/core/server/data/exporter/table-lists.js +1 -0
  85. package/core/server/data/migrations/utils/settings.js +1 -3
  86. package/core/server/data/migrations/versions/5.15/2022-09-12-16-10-add-posts-lexical-column.js +8 -0
  87. package/core/server/data/migrations/versions/5.15/2022-09-14-12-46-add-email-track-clicks-setting.js +8 -0
  88. package/core/server/data/migrations/versions/5.15/2022-09-16-08-22-add-post-revisions-table.js +9 -0
  89. package/core/server/data/schema/default-settings/default-settings.json +8 -0
  90. package/core/server/data/schema/schema.js +8 -0
  91. package/core/server/lib/lexical.js +12 -0
  92. package/core/server/models/base/plugins/user-type.js +4 -6
  93. package/core/server/models/post-revision.js +35 -0
  94. package/core/server/models/post.js +72 -7
  95. package/core/server/services/bulk-email/bulk-email-processor.js +2 -5
  96. package/core/server/services/{redirects → custom-redirects}/api.js +0 -0
  97. package/core/server/services/{redirects → custom-redirects}/index.js +0 -0
  98. package/core/server/services/{redirects → custom-redirects}/utils.js +0 -0
  99. package/core/server/services/{redirects → custom-redirects}/validation.js +0 -0
  100. package/core/server/services/explore/service.js +5 -3
  101. package/core/server/services/link-click-tracking/index.js +25 -0
  102. package/core/server/services/link-redirection/index.js +33 -0
  103. package/core/server/services/link-replacement/index.js +24 -0
  104. package/core/server/services/mega/email-preview.js +7 -0
  105. package/core/server/services/mega/mega.js +1 -1
  106. package/core/server/services/mega/post-email-serializer.js +75 -27
  107. package/core/server/services/members/api.js +0 -2
  108. package/core/server/services/permissions/index.js +1 -2
  109. package/core/server/services/posts/posts-service.js +7 -16
  110. package/core/server/services/posts/stats/post-stats.js +35 -0
  111. package/core/server/services/staff/index.js +10 -1
  112. package/core/server/services/url/config.js +2 -0
  113. package/core/shared/config/defaults.json +2 -2
  114. package/core/shared/config/overrides.json +3 -2
  115. package/core/shared/labs.js +4 -2
  116. package/package.json +97 -90
  117. package/yarn.lock +395 -198
  118. package/components/tryghost-api-framework-5.14.2.tgz +0 -0
  119. package/components/tryghost-api-version-compatibility-service-5.14.2.tgz +0 -0
  120. package/components/tryghost-bootstrap-socket-5.14.2.tgz +0 -0
  121. package/components/tryghost-constants-5.14.2.tgz +0 -0
  122. package/components/tryghost-domain-events-5.14.2.tgz +0 -0
  123. package/components/tryghost-email-analytics-provider-mailgun-5.14.2.tgz +0 -0
  124. package/components/tryghost-email-content-generator-5.14.2.tgz +0 -0
  125. package/components/tryghost-express-dynamic-redirects-5.14.2.tgz +0 -0
  126. package/components/tryghost-extract-api-key-5.14.2.tgz +0 -0
  127. package/components/tryghost-html-to-plaintext-5.14.2.tgz +0 -0
  128. package/components/tryghost-mailgun-client-5.14.2.tgz +0 -0
  129. package/components/tryghost-member-analytics-service-5.14.2.tgz +0 -0
  130. package/components/tryghost-member-attribution-5.14.2.tgz +0 -0
  131. package/components/tryghost-member-events-5.14.2.tgz +0 -0
  132. package/components/tryghost-members-analytics-ingress-5.14.2.tgz +0 -0
  133. package/components/tryghost-members-api-5.14.2.tgz +0 -0
  134. package/components/tryghost-members-csv-5.14.2.tgz +0 -0
  135. package/components/tryghost-members-events-service-5.14.2.tgz +0 -0
  136. package/components/tryghost-members-importer-5.14.2.tgz +0 -0
  137. package/components/tryghost-members-offers-5.14.2.tgz +0 -0
  138. package/components/tryghost-members-stripe-service-5.14.2.tgz +0 -0
  139. package/components/tryghost-minifier-5.14.2.tgz +0 -0
  140. package/components/tryghost-mw-api-version-mismatch-5.14.2.tgz +0 -0
  141. package/components/tryghost-mw-cache-control-5.14.2.tgz +0 -0
  142. package/components/tryghost-mw-session-from-token-5.14.2.tgz +0 -0
  143. package/components/tryghost-mw-update-user-last-seen-5.14.2.tgz +0 -0
  144. package/components/tryghost-mw-vhost-5.14.2.tgz +0 -0
  145. package/components/tryghost-security-5.14.2.tgz +0 -0
  146. package/components/tryghost-staff-service-5.14.2.tgz +0 -0
  147. package/core/built/admin/assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css +0 -1
  148. package/core/built/admin/assets/ghost-dark-13b669d50f494edf24d832b32ece2177.css +0 -1
  149. package/core/server/services/permissions/public.js +0 -76
@@ -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
@@ -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
  };
@@ -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
  }
@@ -7,10 +7,10 @@ const mobiledoc = require('../../../../../lib/mobiledoc');
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
  }
@@ -101,8 +101,8 @@ module.exports = {
101
101
  * - user exists? admin api access
102
102
  */
103
103
  if (localUtils.isContentAPI(frame)) {
104
- // CASE: the content api endpoint for posts should not return mobiledoc
105
- removeMobiledocFormat(frame);
104
+ // CASE: the content api endpoint for posts should not return mobiledoc or lexical
105
+ removeSourceFormats(frame);
106
106
 
107
107
  setDefaultOrder(frame);
108
108
  forceVisibilityColumn(frame);
@@ -127,8 +127,8 @@ module.exports = {
127
127
  * - user exists? admin api access
128
128
  */
129
129
  if (localUtils.isContentAPI(frame)) {
130
- // CASE: the content api endpoint for posts should not return mobiledoc
131
- removeMobiledocFormat(frame);
130
+ // CASE: the content api endpoint for posts should not return mobiledoc or lexical
131
+ removeSourceFormats(frame);
132
132
 
133
133
  setDefaultOrder(frame);
134
134
  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',
@@ -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
  };
@@ -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.posts?.[0]) {
39
+ return;
40
+ }
41
+
42
+ const [post] = frame.data.posts;
43
+ if (post.mobiledoc && post.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
  };
@@ -21,6 +21,7 @@ const BACKUP_TABLES = [
21
21
  'tokens',
22
22
  'sessions',
23
23
  'mobiledoc_revisions',
24
+ 'post_revisions',
24
25
  'email_batches',
25
26
  'email_recipients',
26
27
  'members_cancel_events',
@@ -32,9 +32,7 @@ function addSetting({key, value, type, group}) {
32
32
  group,
33
33
  type,
34
34
  created_at: now,
35
- created_by: MIGRATION_USER,
36
- updated_at: now,
37
- updated_by: MIGRATION_USER
35
+ created_by: MIGRATION_USER
38
36
  });
39
37
  },
40
38
  async function down(connection) {
@@ -0,0 +1,8 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('posts', 'lexical', {
4
+ type: 'text',
5
+ maxlength: 1000000000,
6
+ fieldtype: 'long',
7
+ nullable: true
8
+ });
@@ -0,0 +1,8 @@
1
+ const {addSetting} = require('../../utils');
2
+
3
+ module.exports = addSetting({
4
+ key: 'email_track_clicks',
5
+ value: 'true',
6
+ type: 'boolean',
7
+ group: 'email'
8
+ });
@@ -0,0 +1,9 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('post_revisions', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ post_id: {type: 'string', maxlength: 24, nullable: false, index: true},
6
+ lexical: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
7
+ created_at_ts: {type: 'bigInteger', nullable: false},
8
+ created_at: {type: 'dateTime', nullable: false}
9
+ });
@@ -360,6 +360,14 @@
360
360
  },
361
361
  "type": "boolean"
362
362
  },
363
+ "email_track_clicks": {
364
+ "defaultValue": "true",
365
+ "validations": {
366
+ "isEmpty": false,
367
+ "isIn": [["true", "false"]]
368
+ },
369
+ "type": "boolean"
370
+ },
363
371
  "email_verification_required": {
364
372
  "defaultValue": "false",
365
373
  "validations": {
@@ -45,6 +45,7 @@ module.exports = {
45
45
  title: {type: 'string', maxlength: 2000, nullable: false, validations: {isLength: {max: 255}}},
46
46
  slug: {type: 'string', maxlength: 191, nullable: false},
47
47
  mobiledoc: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
48
+ lexical: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
48
49
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
49
50
  comment_id: {type: 'string', maxlength: 50, nullable: true},
50
51
  plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
@@ -386,6 +387,13 @@ module.exports = {
386
387
  created_at_ts: {type: 'bigInteger', nullable: false},
387
388
  created_at: {type: 'dateTime', nullable: false}
388
389
  },
390
+ post_revisions: {
391
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
392
+ post_id: {type: 'string', maxlength: 24, nullable: false, index: true},
393
+ lexical: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
394
+ created_at_ts: {type: 'bigInteger', nullable: false},
395
+ created_at: {type: 'dateTime', nullable: false}
396
+ },
389
397
  members: {
390
398
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
391
399
  uuid: {type: 'string', maxlength: 36, nullable: true, unique: true, validations: {isUUID: true}},
@@ -0,0 +1,12 @@
1
+ let lexicalHtmlRenderer;
2
+
3
+ module.exports = {
4
+ get lexicalHtmlRenderer() {
5
+ if (!lexicalHtmlRenderer) {
6
+ const LexicalHtmlRenderer = require('@tryghost/kg-lexical-html-renderer');
7
+ lexicalHtmlRenderer = new LexicalHtmlRenderer();
8
+ }
9
+
10
+ return lexicalHtmlRenderer;
11
+ }
12
+ };
@@ -39,13 +39,11 @@ module.exports = function (Bookshelf) {
39
39
  /**
40
40
  * @NOTE:
41
41
  *
42
- * This is a dirty hotfix for v0.1 only.
43
- * The `x_by` columns are getting deprecated soon (https://github.com/TryGhost/Ghost/issues/10286).
42
+ * This is a dirty fix until we get rid of all the x_by columns
43
+ * @deprecated x_by columns are deprecated as of v1.0 - instead we should use the actions table
44
+ * see https://github.com/TryGhost/Ghost/issues/10286.
44
45
  *
45
- * We return the owner ID '1' in case an integration updates or creates
46
- * resources. v0.1 will continue to use the `x_by` columns. v0.1 does not support integrations.
47
- * API v2 will introduce a new feature to solve inserting/updating resources
48
- * from users or integrations. API v2 won't expose `x_by` columns anymore.
46
+ * We return the owner ID '1' in case an integration updates or creates resources.
49
47
  *
50
48
  * ---
51
49
  *
@@ -0,0 +1,35 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const PostRevision = ghostBookshelf.Model.extend({
4
+ tableName: 'post_revisions'
5
+ }, {
6
+ permittedOptions(methodName) {
7
+ let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
8
+ const validOptions = {
9
+ findAll: ['filter', 'columns']
10
+ };
11
+
12
+ if (validOptions[methodName]) {
13
+ options = options.concat(validOptions[methodName]);
14
+ }
15
+
16
+ return options;
17
+ },
18
+
19
+ orderDefaultRaw() {
20
+ return 'created_at_ts DESC';
21
+ },
22
+
23
+ toJSON(unfilteredOptions) {
24
+ const options = PostRevision.filterOptions(unfilteredOptions, 'toJSON');
25
+ const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
26
+
27
+ // CASE: only for internal accuracy
28
+ delete attrs.created_at_ts;
29
+ return attrs;
30
+ }
31
+ });
32
+
33
+ module.exports = {
34
+ PostRevision: ghostBookshelf.model('PostRevision', PostRevision)
35
+ };
@@ -13,6 +13,7 @@ const config = require('../../shared/config');
13
13
  const settingsCache = require('../../shared/settings-cache');
14
14
  const limitService = require('../services/limits');
15
15
  const mobiledocLib = require('../lib/mobiledoc');
16
+ const lexicalLib = require('../lib/lexical');
16
17
  const relations = require('./relations');
17
18
  const urlUtils = require('../../shared/url-utils');
18
19
  const {Tag} = require('./tag');
@@ -29,6 +30,7 @@ const messages = {
29
30
  };
30
31
 
31
32
  const MOBILEDOC_REVISIONS_COUNT = 10;
33
+ const POST_REVISIONS_COUNT = 10;
32
34
  const ALL_STATUSES = ['published', 'draft', 'scheduled', 'sent'];
33
35
 
34
36
  let Post;
@@ -90,7 +92,7 @@ Post = ghostBookshelf.Model.extend({
90
92
  };
91
93
  },
92
94
 
93
- relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta', 'tiers'],
95
+ relationships: ['tags', 'authors', 'mobiledoc_revisions', 'post_revisions', 'posts_meta', 'tiers'],
94
96
 
95
97
  // NOTE: look up object, not super nice, but was easy to implement
96
98
  relationshipBelongsTo: {
@@ -596,7 +598,7 @@ Post = ghostBookshelf.Model.extend({
596
598
  });
597
599
  }
598
600
 
599
- if (!this.get('mobiledoc')) {
601
+ if (!this.get('mobiledoc') && !this.get('lexical')) {
600
602
  this.set('mobiledoc', JSON.stringify(mobiledocLib.blankDocument));
601
603
  }
602
604
 
@@ -610,9 +612,12 @@ Post = ghostBookshelf.Model.extend({
610
612
  // CASE: ?force_rerender=true passed via Admin API
611
613
  // CASE: html is null, but mobiledoc exists (only important for migrations & importing)
612
614
  if (
613
- this.hasChanged('mobiledoc')
614
- || options.force_rerender
615
- || (!this.get('html') && (options.migrating || options.importing))
615
+ !this.get('lexical') &&
616
+ (
617
+ this.hasChanged('mobiledoc')
618
+ || options.force_rerender
619
+ || (!this.get('html') && (options.migrating || options.importing))
620
+ )
616
621
  ) {
617
622
  try {
618
623
  this.set('html', mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(this.get('mobiledoc'))));
@@ -624,6 +629,28 @@ Post = ghostBookshelf.Model.extend({
624
629
  }
625
630
  }
626
631
 
632
+ // CASE: lexical has changed, generate html
633
+ // CASE: ?force_rerender=true passed via Admin API
634
+ // CASE: html is null, but lexical exists (only important for migrations & importing)
635
+ if (
636
+ !this.get('mobiledoc') &&
637
+ (
638
+ this.hasChanged('lexical')
639
+ || options.force_rerender
640
+ || (!this.get('html') && (options.migrating || options.importing))
641
+ )
642
+ ) {
643
+ try {
644
+ this.set('html', lexicalLib.lexicalHtmlRenderer.render(this.get('lexical')));
645
+ } catch (err) {
646
+ throw new errors.ValidationError({
647
+ message: 'Invalid lexical structure.',
648
+ help: 'https://ghost.org/docs/publishing/',
649
+ property: 'lexical'
650
+ });
651
+ }
652
+ }
653
+
627
654
  if (this.hasChanged('html') || !this.get('plaintext')) {
628
655
  let plaintext;
629
656
 
@@ -750,7 +777,7 @@ Post = ghostBookshelf.Model.extend({
750
777
  }
751
778
 
752
779
  // CASE: Handle mobiledoc backups/revisions. This is a pure database feature.
753
- if (model.hasChanged('mobiledoc') && !options.importing && !options.migrating) {
780
+ if (model.hasChanged('mobiledoc') && !model.get('lexical') && !options.importing && !options.migrating) {
754
781
  ops.push(function updateRevisions() {
755
782
  return ghostBookshelf.model('MobiledocRevision')
756
783
  .findAll(Object.assign({
@@ -794,6 +821,39 @@ Post = ghostBookshelf.Model.extend({
794
821
  });
795
822
  }
796
823
 
824
+ // CASE: Handle post backups/revisions. This is a pure database feature.
825
+ if (model.hasChanged('lexical') && !model.get('mobiledoc') && !options.importing && !options.migrating) {
826
+ ops.push(function updateRevisions() {
827
+ return ghostBookshelf.model('PostRevision')
828
+ .findAll(Object.assign({
829
+ filter: `post_id:${model.id}`,
830
+ columns: ['id']
831
+ }, _.pick(options, 'transacting')))
832
+ .then((revisions) => {
833
+ // Store previous + latest lexical content
834
+ if (!revisions.length && options.method !== 'insert') {
835
+ model.set('post_revisions', [{
836
+ post_id: model.id,
837
+ lexical: model.previous('lexical'),
838
+ created_at_ts: Date.now() - 1
839
+ }, {
840
+ post_id: model.id,
841
+ lexical: model.get('lexical'),
842
+ created_at_ts: Date.now()
843
+ }]);
844
+ } else {
845
+ const revisionsJSON = revisions.toJSON().slice(0, POST_REVISIONS_COUNT - 1);
846
+
847
+ model.set('post_revisions', revisionsJSON.concat([{
848
+ post_id: model.id,
849
+ lexical: model.get('lexical'),
850
+ created_at_ts: Date.now()
851
+ }]));
852
+ }
853
+ });
854
+ });
855
+ }
856
+
797
857
  if (this.get('tiers')) {
798
858
  this.set('tiers', this.get('tiers').map(t => ({
799
859
  id: t.id
@@ -831,6 +891,10 @@ Post = ghostBookshelf.Model.extend({
831
891
  return this.hasMany('MobiledocRevision', 'post_id');
832
892
  },
833
893
 
894
+ post_revisions() {
895
+ return this.hasMany('PostRevision', 'post_id');
896
+ },
897
+
834
898
  posts_meta: function postsMeta() {
835
899
  return this.hasOne('PostsMeta', 'post_id');
836
900
  },
@@ -901,6 +965,7 @@ Post = ghostBookshelf.Model.extend({
901
965
 
902
966
  // CASE: never expose the revisions
903
967
  delete attrs.mobiledoc_revisions;
968
+ delete attrs.post_revisions;
904
969
 
905
970
  // If the current column settings allow it...
906
971
  if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
@@ -974,7 +1039,7 @@ Post = ghostBookshelf.Model.extend({
974
1039
  return filter;
975
1040
  }
976
1041
  }, {
977
- allowedFormats: ['mobiledoc', 'html', 'plaintext'],
1042
+ allowedFormats: ['mobiledoc', 'lexical', 'html', 'plaintext'],
978
1043
 
979
1044
  orderDefaultOptions: function orderDefaultOptions() {
980
1045
  return {
@@ -7,7 +7,6 @@ const logging = require('@tryghost/logging');
7
7
  const models = require('../../models');
8
8
  const MailgunClient = require('@tryghost/mailgun-client');
9
9
  const sentry = require('../../../shared/sentry');
10
- const labs = require('../../../shared/labs');
11
10
  const debug = require('@tryghost/debug')('mega');
12
11
  const postEmailSerializer = require('../mega/post-email-serializer');
13
12
  const configService = require('../../../shared/config');
@@ -173,10 +172,8 @@ module.exports = {
173
172
  // Load newsletter data on email
174
173
  await emailBatchModel.relations.email.getLazyRelation('newsletter', {require: false, ...knexOptions});
175
174
 
176
- if (labs.isSet('newsletterPaywall')) {
177
- // Load post data on email - for content gating on paywall
178
- await emailBatchModel.relations.email.getLazyRelation('post', {require: false, ...knexOptions});
179
- }
175
+ // Load post data on email - for content gating on paywall
176
+ await emailBatchModel.relations.email.getLazyRelation('post', {require: false, ...knexOptions});
180
177
 
181
178
  // send the email
182
179
  const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);