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
@@ -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',
@@ -37,7 +38,9 @@ const BACKUP_TABLES = [
37
38
  'comments',
38
39
  'comment_likes',
39
40
  'comment_reports',
40
- 'jobs'
41
+ 'jobs',
42
+ 'link_redirects',
43
+ 'members_link_click_events'
41
44
  ];
42
45
 
43
46
  // NOTE: exposing only tables which are going to be included in a "default" export file
@@ -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
+ });
@@ -0,0 +1,10 @@
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
+ });
@@ -0,0 +1,8 @@
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
+ });
@@ -0,0 +1,21 @@
1
+ const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ createAddColumnMigration('members_created_events', 'referrer_source', {
5
+ type: 'string',
6
+ maxlength: 191,
7
+ nullable: true
8
+ }),
9
+
10
+ createAddColumnMigration('members_created_events', 'referrer_medium', {
11
+ type: 'string',
12
+ maxlength: 191,
13
+ nullable: true
14
+ }),
15
+
16
+ createAddColumnMigration('members_created_events', 'referrer_url', {
17
+ type: 'string',
18
+ maxlength: 2000,
19
+ nullable: true
20
+ })
21
+ );
@@ -0,0 +1,21 @@
1
+ const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ createAddColumnMigration('members_subscription_created_events', 'referrer_source', {
5
+ type: 'string',
6
+ maxlength: 191,
7
+ nullable: true
8
+ }),
9
+
10
+ createAddColumnMigration('members_subscription_created_events', 'referrer_medium', {
11
+ type: 'string',
12
+ maxlength: 191,
13
+ nullable: true
14
+ }),
15
+
16
+ createAddColumnMigration('members_subscription_created_events', 'referrer_url', {
17
+ type: 'string',
18
+ maxlength: 2000,
19
+ nullable: true
20
+ })
21
+ );
@@ -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}},
@@ -489,6 +497,9 @@ module.exports = {
489
497
  }
490
498
  },
491
499
  attribution_url: {type: 'string', maxlength: 2000, nullable: true},
500
+ referrer_source: {type: 'string', maxlength: 191, nullable: true},
501
+ referrer_medium: {type: 'string', maxlength: 191, nullable: true},
502
+ referrer_url: {type: 'string', maxlength: 2000, nullable: true},
492
503
  source: {
493
504
  type: 'string', maxlength: 50, nullable: false, validations: {
494
505
  isIn: [['member', 'import', 'system', 'api', 'admin']]
@@ -626,7 +637,10 @@ module.exports = {
626
637
  isIn: [['url', 'post', 'page', 'author', 'tag']]
627
638
  }
628
639
  },
629
- attribution_url: {type: 'string', maxlength: 2000, nullable: true}
640
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true},
641
+ referrer_source: {type: 'string', maxlength: 191, nullable: true},
642
+ referrer_medium: {type: 'string', maxlength: 191, nullable: true},
643
+ referrer_url: {type: 'string', maxlength: 2000, nullable: true}
630
644
  },
631
645
  offer_redemptions: {
632
646
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -821,5 +835,19 @@ module.exports = {
821
835
  finished_at: {type: 'dateTime', nullable: true},
822
836
  created_at: {type: 'dateTime', nullable: false},
823
837
  updated_at: {type: 'dateTime', nullable: true}
838
+ },
839
+ link_redirects: {
840
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
841
+ from: {type: 'string', maxlength: 2000, nullable: false},
842
+ to: {type: 'string', maxlength: 2000, nullable: false},
843
+ post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
844
+ created_at: {type: 'dateTime', nullable: false},
845
+ updated_at: {type: 'dateTime', nullable: true}
846
+ },
847
+ members_link_click_events: {
848
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
849
+ 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
+ created_at: {type: 'dateTime', nullable: false}
824
852
  }
825
853
  };
@@ -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,65 @@
1
+ const ghostBookshelf = require('./base');
2
+ const urlUtils = require('../../shared/url-utils');
3
+
4
+ const LinkRedirect = ghostBookshelf.Model.extend({
5
+ tableName: 'link_redirects',
6
+
7
+ post() {
8
+ return this.belongsTo('Post', 'post_id');
9
+ },
10
+
11
+ formatOnWrite(attrs) {
12
+ if (attrs.to) {
13
+ attrs.to = urlUtils.absoluteToTransformReady(attrs.to);
14
+ }
15
+
16
+ return attrs;
17
+ },
18
+
19
+ parse() {
20
+ const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
21
+
22
+ if (attrs.to) {
23
+ attrs.to = urlUtils.transformReadyToAbsolute(attrs.to);
24
+ }
25
+
26
+ return attrs;
27
+ }
28
+ }, {
29
+ orderDefaultRaw(options) {
30
+ if (options.withRelated && options.withRelated.includes('count.clicks')) {
31
+ return '`count__clicks` DESC, `to` DESC';
32
+ }
33
+ return '`to` DESC';
34
+ },
35
+
36
+ permittedOptions(methodName) {
37
+ let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
38
+ const validOptions = {
39
+ findAll: ['filter', 'columns', 'withRelated']
40
+ };
41
+
42
+ if (validOptions[methodName]) {
43
+ options = options.concat(validOptions[methodName]);
44
+ }
45
+
46
+ return options;
47
+ },
48
+
49
+ countRelations() {
50
+ return {
51
+ clicks(modelOrCollection) {
52
+ modelOrCollection.query('columns', 'link_redirects.*', (qb) => {
53
+ qb.countDistinct('members_link_click_events.member_id')
54
+ .from('members_link_click_events')
55
+ .whereRaw('link_redirects.id = members_link_click_events.link_id')
56
+ .as('count__clicks');
57
+ });
58
+ }
59
+ };
60
+ }
61
+ });
62
+
63
+ module.exports = {
64
+ LinkRedirect: ghostBookshelf.model('LinkRedirect', LinkRedirect)
65
+ };
@@ -0,0 +1,26 @@
1
+ const errors = require('@tryghost/errors');
2
+ const ghostBookshelf = require('./base');
3
+
4
+ const MemberLinkClickEvent = ghostBookshelf.Model.extend({
5
+ tableName: 'members_link_click_events',
6
+
7
+ link() {
8
+ return this.belongsTo('LinkRedirect', 'link_id');
9
+ },
10
+
11
+ member() {
12
+ return this.belongsTo('Member', 'member_id', 'id');
13
+ }
14
+ }, {
15
+ async edit() {
16
+ throw new errors.IncorrectUsageError({message: 'Cannot edit MemberLinkClickEvent'});
17
+ },
18
+
19
+ async destroy() {
20
+ throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberLinkClickEvent'});
21
+ }
22
+ });
23
+
24
+ module.exports = {
25
+ MemberLinkClickEvent: ghostBookshelf.model('MemberLinkClickEvent', MemberLinkClickEvent)
26
+ };
@@ -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');
@@ -25,10 +26,15 @@ const messages = {
25
26
  expectedPublishedAtInFuture: 'Date must be at least {cannotScheduleAPostBeforeInMinutes} minutes in the future.',
26
27
  untitled: '(Untitled)',
27
28
  notEnoughPermission: 'You do not have permission to perform this action',
28
- invalidNewsletter: 'The newsletter parameter doesn\'t match any active newsletter.'
29
+ invalidNewsletter: 'The newsletter parameter doesn\'t match any active newsletter.',
30
+ invalidMobiledocStructure: 'Invalid mobiledoc structure.',
31
+ invalidMobiledocStructureHelp: 'https://ghost.org/docs/publishing/',
32
+ invalidLexicalStructure: 'Invalid lexical structure.',
33
+ invalidLexicalStructureHelp: 'https://ghost.org/docs/publishing/'
29
34
  };
30
35
 
31
36
  const MOBILEDOC_REVISIONS_COUNT = 10;
37
+ const POST_REVISIONS_COUNT = 10;
32
38
  const ALL_STATUSES = ['published', 'draft', 'scheduled', 'sent'];
33
39
 
34
40
  let Post;
@@ -90,7 +96,7 @@ Post = ghostBookshelf.Model.extend({
90
96
  };
91
97
  },
92
98
 
93
- relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta', 'tiers'],
99
+ relationships: ['tags', 'authors', 'mobiledoc_revisions', 'post_revisions', 'posts_meta', 'tiers'],
94
100
 
95
101
  // NOTE: look up object, not super nice, but was easy to implement
96
102
  relationshipBelongsTo: {
@@ -128,6 +134,7 @@ Post = ghostBookshelf.Model.extend({
128
134
  // transform URLs from __GHOST_URL__ to absolute
129
135
  [
130
136
  'mobiledoc',
137
+ 'lexical',
131
138
  'html',
132
139
  'plaintext',
133
140
  'custom_excerpt',
@@ -156,6 +163,7 @@ Post = ghostBookshelf.Model.extend({
156
163
  cardTransformers: mobiledocLib.cards
157
164
  }
158
165
  },
166
+ lexical: 'lexicalToTransformReady',
159
167
  html: 'htmlToTransformReady',
160
168
  plaintext: 'plaintextToTransformReady',
161
169
  custom_excerpt: 'htmlToTransformReady',
@@ -596,7 +604,7 @@ Post = ghostBookshelf.Model.extend({
596
604
  });
597
605
  }
598
606
 
599
- if (!this.get('mobiledoc')) {
607
+ if (!this.get('mobiledoc') && !this.get('lexical')) {
600
608
  this.set('mobiledoc', JSON.stringify(mobiledocLib.blankDocument));
601
609
  }
602
610
 
@@ -610,20 +618,46 @@ Post = ghostBookshelf.Model.extend({
610
618
  // CASE: ?force_rerender=true passed via Admin API
611
619
  // CASE: html is null, but mobiledoc exists (only important for migrations & importing)
612
620
  if (
613
- this.hasChanged('mobiledoc')
614
- || options.force_rerender
615
- || (!this.get('html') && (options.migrating || options.importing))
621
+ !this.get('lexical') &&
622
+ (
623
+ this.hasChanged('mobiledoc')
624
+ || options.force_rerender
625
+ || (!this.get('html') && (options.migrating || options.importing))
626
+ )
616
627
  ) {
617
628
  try {
618
629
  this.set('html', mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(this.get('mobiledoc'))));
619
630
  } catch (err) {
620
631
  throw new errors.ValidationError({
621
- message: 'Invalid mobiledoc structure.',
632
+ message: tpl(messages.invalidMobiledocStructure),
622
633
  help: 'https://ghost.org/docs/publishing/'
623
634
  });
624
635
  }
625
636
  }
626
637
 
638
+ // CASE: lexical has changed, generate html
639
+ // CASE: ?force_rerender=true passed via Admin API
640
+ // CASE: html is null, but lexical exists (only important for migrations & importing)
641
+ if (
642
+ !this.get('mobiledoc') &&
643
+ (
644
+ this.hasChanged('lexical')
645
+ || options.force_rerender
646
+ || (!this.get('html') && (options.migrating || options.importing))
647
+ )
648
+ ) {
649
+ try {
650
+ this.set('html', lexicalLib.lexicalHtmlRenderer.render(this.get('lexical')));
651
+ } catch (err) {
652
+ throw new errors.ValidationError({
653
+ message: tpl(messages.invalidLexicalStructure),
654
+ context: err.message,
655
+ property: 'lexical',
656
+ help: tpl(messages.invalidLexicalStructureHelp)
657
+ });
658
+ }
659
+ }
660
+
627
661
  if (this.hasChanged('html') || !this.get('plaintext')) {
628
662
  let plaintext;
629
663
 
@@ -750,7 +784,7 @@ Post = ghostBookshelf.Model.extend({
750
784
  }
751
785
 
752
786
  // CASE: Handle mobiledoc backups/revisions. This is a pure database feature.
753
- if (model.hasChanged('mobiledoc') && !options.importing && !options.migrating) {
787
+ if (model.hasChanged('mobiledoc') && !model.get('lexical') && !options.importing && !options.migrating) {
754
788
  ops.push(function updateRevisions() {
755
789
  return ghostBookshelf.model('MobiledocRevision')
756
790
  .findAll(Object.assign({
@@ -794,6 +828,39 @@ Post = ghostBookshelf.Model.extend({
794
828
  });
795
829
  }
796
830
 
831
+ // CASE: Handle post backups/revisions. This is a pure database feature.
832
+ if (model.hasChanged('lexical') && !model.get('mobiledoc') && !options.importing && !options.migrating) {
833
+ ops.push(function updateRevisions() {
834
+ return ghostBookshelf.model('PostRevision')
835
+ .findAll(Object.assign({
836
+ filter: `post_id:${model.id}`,
837
+ columns: ['id']
838
+ }, _.pick(options, 'transacting')))
839
+ .then((revisions) => {
840
+ // Store previous + latest lexical content
841
+ if (!revisions.length && options.method !== 'insert') {
842
+ model.set('post_revisions', [{
843
+ post_id: model.id,
844
+ lexical: model.previous('lexical'),
845
+ created_at_ts: Date.now() - 1
846
+ }, {
847
+ post_id: model.id,
848
+ lexical: model.get('lexical'),
849
+ created_at_ts: Date.now()
850
+ }]);
851
+ } else {
852
+ const revisionsJSON = revisions.toJSON().slice(0, POST_REVISIONS_COUNT - 1);
853
+
854
+ model.set('post_revisions', revisionsJSON.concat([{
855
+ post_id: model.id,
856
+ lexical: model.get('lexical'),
857
+ created_at_ts: Date.now()
858
+ }]));
859
+ }
860
+ });
861
+ });
862
+ }
863
+
797
864
  if (this.get('tiers')) {
798
865
  this.set('tiers', this.get('tiers').map(t => ({
799
866
  id: t.id
@@ -831,6 +898,10 @@ Post = ghostBookshelf.Model.extend({
831
898
  return this.hasMany('MobiledocRevision', 'post_id');
832
899
  },
833
900
 
901
+ post_revisions() {
902
+ return this.hasMany('PostRevision', 'post_id');
903
+ },
904
+
834
905
  posts_meta: function postsMeta() {
835
906
  return this.hasOne('PostsMeta', 'post_id');
836
907
  },
@@ -901,6 +972,7 @@ Post = ghostBookshelf.Model.extend({
901
972
 
902
973
  // CASE: never expose the revisions
903
974
  delete attrs.mobiledoc_revisions;
975
+ delete attrs.post_revisions;
904
976
 
905
977
  // If the current column settings allow it...
906
978
  if (!options.columns || (options.columns && options.columns.indexOf('primary_tag') > -1)) {
@@ -974,7 +1046,7 @@ Post = ghostBookshelf.Model.extend({
974
1046
  return filter;
975
1047
  }
976
1048
  }, {
977
- allowedFormats: ['mobiledoc', 'html', 'plaintext'],
1049
+ allowedFormats: ['mobiledoc', 'lexical', 'html', 'plaintext'],
978
1050
 
979
1051
  orderDefaultOptions: function orderDefaultOptions() {
980
1052
  return {
@@ -1273,6 +1345,15 @@ Post = ghostBookshelf.Model.extend({
1273
1345
  .whereRaw('posts.id = members_subscription_created_events.attribution_id')
1274
1346
  .as('count__conversions');
1275
1347
  });
1348
+ },
1349
+ clicks(modelOrCollection) {
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')
1355
+ .as('count__clicks');
1356
+ });
1276
1357
  }
1277
1358
  };
1278
1359
  }
@@ -7,14 +7,13 @@ 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');
14
13
  const settingsCache = require('../../../shared/settings-cache');
15
14
 
16
15
  const messages = {
17
- error: 'The email service was unable to send an email batch.'
16
+ error: 'The email service received an error from mailgun and was unable to send.'
18
17
  };
19
18
 
20
19
  const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
@@ -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);
@@ -254,11 +251,13 @@ module.exports = {
254
251
  const response = await mailgunClient.send(emailData, recipientData, replacements);
255
252
  debug(`sent message (${Date.now() - startTime}ms)`);
256
253
  return response;
257
- } catch (error) {
258
- // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
254
+ } catch (err) {
259
255
  let ghostError = new errors.EmailError({
260
- err: error,
261
- context: tpl(messages.error),
256
+ err,
257
+ message: tpl(messages.error),
258
+ context: `Mailgun Error ${err.error.status}: ${err.error.details}`,
259
+ // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
260
+ help: `https://ghost.org/docs/newsletters/#bulk-email-configuration`,
262
261
  code: 'BULK_EMAIL_SEND_FAILED'
263
262
  });
264
263