ghost 5.33.7 → 5.34.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 (175) hide show
  1. package/components/tryghost-adapter-cache-redis-5.34.0.tgz +0 -0
  2. package/components/tryghost-adapter-manager-5.34.0.tgz +0 -0
  3. package/components/{tryghost-api-framework-5.33.7.tgz → tryghost-api-framework-5.34.0.tgz} +0 -0
  4. package/components/tryghost-api-version-compatibility-service-5.34.0.tgz +0 -0
  5. package/components/tryghost-audience-feedback-5.34.0.tgz +0 -0
  6. package/components/tryghost-bootstrap-socket-5.34.0.tgz +0 -0
  7. package/components/tryghost-constants-5.34.0.tgz +0 -0
  8. package/components/tryghost-custom-theme-settings-service-5.34.0.tgz +0 -0
  9. package/components/tryghost-data-generator-5.34.0.tgz +0 -0
  10. package/components/tryghost-domain-events-5.34.0.tgz +0 -0
  11. package/components/tryghost-dynamic-routing-events-5.34.0.tgz +0 -0
  12. package/components/tryghost-email-analytics-provider-mailgun-5.34.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-service-5.34.0.tgz +0 -0
  14. package/components/tryghost-email-content-generator-5.34.0.tgz +0 -0
  15. package/components/tryghost-email-events-5.34.0.tgz +0 -0
  16. package/components/tryghost-email-service-5.34.0.tgz +0 -0
  17. package/components/tryghost-email-suppression-list-5.34.0.tgz +0 -0
  18. package/components/{tryghost-express-dynamic-redirects-5.33.7.tgz → tryghost-express-dynamic-redirects-5.34.0.tgz} +0 -0
  19. package/components/tryghost-extract-api-key-5.34.0.tgz +0 -0
  20. package/components/tryghost-html-to-plaintext-5.34.0.tgz +0 -0
  21. package/components/tryghost-i18n-5.34.0.tgz +0 -0
  22. package/components/tryghost-importer-revue-5.34.0.tgz +0 -0
  23. package/components/{tryghost-job-manager-5.33.7.tgz → tryghost-job-manager-5.34.0.tgz} +0 -0
  24. package/components/tryghost-link-redirects-5.34.0.tgz +0 -0
  25. package/components/tryghost-link-replacer-5.34.0.tgz +0 -0
  26. package/components/{tryghost-link-tracking-5.33.7.tgz → tryghost-link-tracking-5.34.0.tgz} +0 -0
  27. package/components/tryghost-magic-link-5.34.0.tgz +0 -0
  28. package/components/tryghost-mailgun-client-5.34.0.tgz +0 -0
  29. package/components/tryghost-member-attribution-5.34.0.tgz +0 -0
  30. package/components/tryghost-member-events-5.34.0.tgz +0 -0
  31. package/components/tryghost-members-api-5.34.0.tgz +0 -0
  32. package/components/tryghost-members-csv-5.34.0.tgz +0 -0
  33. package/components/{tryghost-members-events-service-5.33.7.tgz → tryghost-members-events-service-5.34.0.tgz} +0 -0
  34. package/components/{tryghost-members-importer-5.33.7.tgz → tryghost-members-importer-5.34.0.tgz} +0 -0
  35. package/components/tryghost-members-offers-5.34.0.tgz +0 -0
  36. package/components/tryghost-members-payments-5.34.0.tgz +0 -0
  37. package/components/{tryghost-members-ssr-5.33.7.tgz → tryghost-members-ssr-5.34.0.tgz} +0 -0
  38. package/components/tryghost-members-stripe-service-5.34.0.tgz +0 -0
  39. package/components/tryghost-milestone-emails-5.34.0.tgz +0 -0
  40. package/components/tryghost-minifier-5.34.0.tgz +0 -0
  41. package/components/tryghost-mw-api-version-mismatch-5.34.0.tgz +0 -0
  42. package/components/tryghost-mw-cache-control-5.34.0.tgz +0 -0
  43. package/components/{tryghost-mw-error-handler-5.33.7.tgz → tryghost-mw-error-handler-5.34.0.tgz} +0 -0
  44. package/components/tryghost-mw-session-from-token-5.34.0.tgz +0 -0
  45. package/components/tryghost-mw-update-user-last-seen-5.34.0.tgz +0 -0
  46. package/components/tryghost-mw-vhost-5.34.0.tgz +0 -0
  47. package/components/tryghost-oembed-service-5.34.0.tgz +0 -0
  48. package/components/{tryghost-package-json-5.33.7.tgz → tryghost-package-json-5.34.0.tgz} +0 -0
  49. package/components/tryghost-referrers-5.34.0.tgz +0 -0
  50. package/components/{tryghost-security-5.33.7.tgz → tryghost-security-5.34.0.tgz} +0 -0
  51. package/components/tryghost-session-service-5.34.0.tgz +0 -0
  52. package/components/{tryghost-settings-path-manager-5.33.7.tgz → tryghost-settings-path-manager-5.34.0.tgz} +0 -0
  53. package/components/tryghost-staff-service-5.34.0.tgz +0 -0
  54. package/components/tryghost-stats-service-5.34.0.tgz +0 -0
  55. package/components/tryghost-tags-public-5.34.0.tgz +0 -0
  56. package/components/tryghost-tiers-5.34.0.tgz +0 -0
  57. package/components/tryghost-update-check-service-5.34.0.tgz +0 -0
  58. package/components/tryghost-verification-trigger-5.34.0.tgz +0 -0
  59. package/components/tryghost-version-notifications-data-service-5.34.0.tgz +0 -0
  60. package/components/tryghost-webmentions-5.34.0.tgz +0 -0
  61. package/core/boot.js +8 -1
  62. package/core/built/admin/assets/{chunk.143.9057a1659075c0ee7336.js → chunk.143.a07da1be864f2e500e18.js} +14 -14
  63. package/core/built/admin/assets/{chunk.178.1d2c848ebe2c7baa38dd.js → chunk.178.9ed5500c900a9032f6c2.js} +4 -4
  64. package/core/built/admin/assets/{chunk.963.e47ead5abeca4cf69fed.js → chunk.616.181e1ad6c33f0bec7a65.js} +3519 -3512
  65. package/core/built/admin/assets/{chunk.963.e47ead5abeca4cf69fed.js.LICENSE.txt → chunk.616.181e1ad6c33f0bec7a65.js.LICENSE.txt} +0 -0
  66. package/core/built/admin/assets/{chunk.79.c3c2c05ea7ff7707fcad.js → chunk.79.ec143a398298020c87e6.js} +112 -112
  67. package/core/built/admin/assets/codemirror/{codemirror-a81c0653d8e57286b75c5a1792f80779.js → codemirror-6c43f4894cbd8db73d7f35cde836c58e.js} +810 -809
  68. package/core/built/admin/assets/{ghost-fb9fb8adbcaf1603ad4006dd2d49e401.css → ghost-558c1e319d6e025bfab2054bc0f7fe83.css} +1 -1
  69. package/core/built/admin/assets/{ghost-904f203ff1f5f6bff4e55a03f39a4fbf.js → ghost-ad40d109653288e74a7cd922341fb33d.js} +115 -90
  70. package/core/built/admin/assets/{ghost-dark-ac0cb221eddc8652a0e7c263ed6513dc.css → ghost-dark-a15754df1f9070dc2525482ce22e2251.css} +1 -1
  71. package/core/built/admin/assets/simplemde/{simplemde-2885e4a40ed66fcae974595584efe50b.js → simplemde-28049a9bd7f432b0648747eb26958a33.js} +836 -836
  72. package/core/built/admin/assets/{vendor-0441964c34d58f2aacd5a04bbe240243.js → vendor-253d6527ca6353855164ef65f896f762.js} +1208 -1233
  73. package/core/built/admin/index.html +6 -6
  74. package/core/cli/generate-data.js +21 -3
  75. package/core/frontend/services/routing/router-manager.js +1 -1
  76. package/core/frontend/web/middleware/handle-image-sizes.js +6 -21
  77. package/core/server/adapters/cache/Redis.js +3 -0
  78. package/core/server/adapters/storage/LocalStorageBase.js +11 -1
  79. package/core/server/api/endpoints/images.js +47 -6
  80. package/core/server/api/endpoints/mentions.js +1 -1
  81. package/core/server/api/endpoints/tags-public.js +2 -1
  82. package/core/server/api/endpoints/utils/serializers/output/email-posts.js +1 -1
  83. package/core/server/api/endpoints/utils/serializers/output/mappers/mentions.js +1 -1
  84. package/core/server/api/endpoints/utils/serializers/output/pages.js +1 -1
  85. package/core/server/api/endpoints/utils/serializers/output/posts.js +1 -1
  86. package/core/server/api/endpoints/utils/serializers/output/previews.js +1 -1
  87. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +1 -0
  88. package/core/server/data/migrations/versions/5.34/2023-01-30-07-27-add-mentions-permission.js +10 -0
  89. package/core/server/data/migrations/versions/5.34/2023-02-08-03-08-add-mentions-notifications-column.js +7 -0
  90. package/core/server/data/migrations/versions/5.34/2023-02-08-22-32-add-mentions-delete-column.js +7 -0
  91. package/core/server/data/schema/fixtures/fixtures.json +9 -2
  92. package/core/server/data/schema/schema.js +3 -1
  93. package/core/server/lib/image/image-size.js +23 -5
  94. package/core/server/lib/lexical.js +36 -3
  95. package/core/server/models/member.js +3 -0
  96. package/core/server/models/mention.js +7 -1
  97. package/core/server/models/post.js +1 -1
  98. package/core/server/models/user.js +4 -1
  99. package/core/server/services/adapter-manager/index.js +7 -0
  100. package/core/server/services/email-analytics/events/StartEmailAnalyticsJobEvent.js +22 -0
  101. package/core/server/services/email-analytics/index.js +2 -22
  102. package/core/server/services/email-analytics/jobs/fetch-latest/index.js +6 -12
  103. package/core/server/services/email-analytics/wrapper.js +99 -0
  104. package/core/server/services/email-service/wrapper.js +3 -14
  105. package/core/server/services/mega/post-email-serializer.js +1 -1
  106. package/core/server/services/members/jobs/index.js +4 -2
  107. package/core/server/services/mentions/BookshelfMentionRepository.js +3 -2
  108. package/core/server/services/mentions/MentionController.js +78 -12
  109. package/core/server/services/mentions/service.js +43 -3
  110. package/core/server/services/milestone-emails/MilestoneQueries.js +58 -0
  111. package/core/server/services/milestone-emails/index.js +1 -0
  112. package/core/server/services/milestone-emails/service.js +58 -0
  113. package/core/server/services/tags-public/index.js +1 -0
  114. package/core/server/services/tags-public/service.js +31 -0
  115. package/core/server/web/api/endpoints/admin/routes.js +0 -1
  116. package/core/server/web/api/middleware/index.js +0 -1
  117. package/core/server/web/shared/middleware/api/spam-prevention.js +31 -1
  118. package/core/server/web/shared/middleware/brute.js +13 -0
  119. package/core/server/web/webmentions/routes.js +3 -0
  120. package/core/shared/config/defaults.json +17 -1
  121. package/core/shared/config/env/config.development.json +0 -3
  122. package/core/shared/config/env/config.testing-browser.json +6 -0
  123. package/core/shared/config/env/config.testing-mysql.json +7 -0
  124. package/core/shared/config/env/config.testing.json +6 -0
  125. package/core/shared/labs.js +3 -3
  126. package/package.json +121 -114
  127. package/yarn.lock +327 -242
  128. package/components/tryghost-adapter-manager-5.33.7.tgz +0 -0
  129. package/components/tryghost-api-version-compatibility-service-5.33.7.tgz +0 -0
  130. package/components/tryghost-audience-feedback-5.33.7.tgz +0 -0
  131. package/components/tryghost-bootstrap-socket-5.33.7.tgz +0 -0
  132. package/components/tryghost-constants-5.33.7.tgz +0 -0
  133. package/components/tryghost-custom-theme-settings-service-5.33.7.tgz +0 -0
  134. package/components/tryghost-data-generator-5.33.7.tgz +0 -0
  135. package/components/tryghost-domain-events-5.33.7.tgz +0 -0
  136. package/components/tryghost-dynamic-routing-events-5.33.7.tgz +0 -0
  137. package/components/tryghost-email-analytics-provider-mailgun-5.33.7.tgz +0 -0
  138. package/components/tryghost-email-analytics-service-5.33.7.tgz +0 -0
  139. package/components/tryghost-email-content-generator-5.33.7.tgz +0 -0
  140. package/components/tryghost-email-events-5.33.7.tgz +0 -0
  141. package/components/tryghost-email-service-5.33.7.tgz +0 -0
  142. package/components/tryghost-email-suppression-list-5.33.7.tgz +0 -0
  143. package/components/tryghost-extract-api-key-5.33.7.tgz +0 -0
  144. package/components/tryghost-html-to-plaintext-5.33.7.tgz +0 -0
  145. package/components/tryghost-i18n-5.33.7.tgz +0 -0
  146. package/components/tryghost-importer-revue-5.33.7.tgz +0 -0
  147. package/components/tryghost-link-redirects-5.33.7.tgz +0 -0
  148. package/components/tryghost-link-replacer-5.33.7.tgz +0 -0
  149. package/components/tryghost-magic-link-5.33.7.tgz +0 -0
  150. package/components/tryghost-mailgun-client-5.33.7.tgz +0 -0
  151. package/components/tryghost-member-attribution-5.33.7.tgz +0 -0
  152. package/components/tryghost-member-events-5.33.7.tgz +0 -0
  153. package/components/tryghost-members-api-5.33.7.tgz +0 -0
  154. package/components/tryghost-members-csv-5.33.7.tgz +0 -0
  155. package/components/tryghost-members-offers-5.33.7.tgz +0 -0
  156. package/components/tryghost-members-payments-5.33.7.tgz +0 -0
  157. package/components/tryghost-members-stripe-service-5.33.7.tgz +0 -0
  158. package/components/tryghost-minifier-5.33.7.tgz +0 -0
  159. package/components/tryghost-mw-api-version-mismatch-5.33.7.tgz +0 -0
  160. package/components/tryghost-mw-cache-control-5.33.7.tgz +0 -0
  161. package/components/tryghost-mw-session-from-token-5.33.7.tgz +0 -0
  162. package/components/tryghost-mw-update-user-last-seen-5.33.7.tgz +0 -0
  163. package/components/tryghost-mw-vhost-5.33.7.tgz +0 -0
  164. package/components/tryghost-oembed-service-5.33.7.tgz +0 -0
  165. package/components/tryghost-referrers-5.33.7.tgz +0 -0
  166. package/components/tryghost-session-service-5.33.7.tgz +0 -0
  167. package/components/tryghost-staff-service-5.33.7.tgz +0 -0
  168. package/components/tryghost-stats-service-5.33.7.tgz +0 -0
  169. package/components/tryghost-tiers-5.33.7.tgz +0 -0
  170. package/components/tryghost-update-check-service-5.33.7.tgz +0 -0
  171. package/components/tryghost-verification-trigger-5.33.7.tgz +0 -0
  172. package/components/tryghost-version-notifications-data-service-5.33.7.tgz +0 -0
  173. package/components/tryghost-webmentions-5.33.7.tgz +0 -0
  174. package/core/server/services/email-analytics/jobs/fetch-latest/run.js +0 -57
  175. package/core/server/web/api/middleware/normalize-image.js +0 -42
@@ -1,4 +1,5 @@
1
1
  const {parentPort} = require('worker_threads');
2
+ const StartEmailAnalyticsJobEvent = require('../../events/StartEmailAnalyticsJobEvent');
2
3
 
3
4
  // recurring job to fetch analytics since the most recently seen event timestamp
4
5
 
@@ -24,22 +25,15 @@ if (parentPort) {
24
25
  }
25
26
 
26
27
  (async () => {
27
- const {run} = require('./run');
28
- const {eventStats, aggregateEndDate, fetchStartDate} = await run({
29
- domainEvents: {
30
- dispatch(event) {
31
- parentPort.postMessage({
32
- event: {
33
- type: event.constructor.name,
34
- data: event
35
- }
36
- });
37
- }
28
+ // We send an evnet message, so that it is emitted on the main thread by the job manager
29
+ // This will start the email analytics job on the main thread (the wrapper service is listening for this event)
30
+ parentPort.postMessage({
31
+ event: {
32
+ type: StartEmailAnalyticsJobEvent.name
38
33
  }
39
34
  });
40
35
 
41
36
  if (parentPort) {
42
- parentPort.postMessage(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate - fetchStartDate}ms`);
43
37
  parentPort.postMessage('done');
44
38
  } else {
45
39
  // give the logging pipes time finish writing before exit
@@ -0,0 +1,99 @@
1
+ const logging = require('@tryghost/logging');
2
+ const debug = require('@tryghost/debug')('jobs:email-analytics:fetch-latest');
3
+
4
+ class EmailAnalyticsServiceWrapper {
5
+ init() {
6
+ if (this.service) {
7
+ return;
8
+ }
9
+
10
+ const {EmailAnalyticsService} = require('@tryghost/email-analytics-service');
11
+ const {EmailEventStorage, EmailEventProcessor} = require('@tryghost/email-service');
12
+ const MailgunProvider = require('@tryghost/email-analytics-provider-mailgun');
13
+ const {EmailRecipientFailure, EmailSpamComplaintEvent} = require('../../models');
14
+ const StartEmailAnalyticsJobEvent = require('./events/StartEmailAnalyticsJobEvent');
15
+
16
+ const domainEvents = require('@tryghost/domain-events');
17
+ const config = require('../../../shared/config');
18
+ const settings = require('../../../shared/settings-cache');
19
+ const db = require('../../data/db');
20
+ const queries = require('./lib/queries');
21
+ const membersService = require('../members');
22
+ const membersRepository = membersService.api.members;
23
+
24
+ this.eventStorage = new EmailEventStorage({
25
+ db,
26
+ membersRepository,
27
+ models: {
28
+ EmailRecipientFailure,
29
+ EmailSpamComplaintEvent
30
+ }
31
+ });
32
+
33
+ // Since this is running in a worker thread, we cant dispatch directly
34
+ // So we post the events as a message to the job manager
35
+ const eventProcessor = new EmailEventProcessor({
36
+ domainEvents,
37
+ db,
38
+ eventStorage: this.eventStorage
39
+ });
40
+
41
+ this.service = new EmailAnalyticsService({
42
+ config,
43
+ settings,
44
+ eventProcessor,
45
+ providers: [
46
+ new MailgunProvider({config, settings})
47
+ ],
48
+ queries
49
+ });
50
+
51
+ // We currently cannot trigger a non-offloaded job from the job manager
52
+ // So the email analytics jobs simply emits an event.
53
+ domainEvents.subscribe(StartEmailAnalyticsJobEvent, async () => {
54
+ await this.startFetch();
55
+ });
56
+ }
57
+
58
+ async fetchLatest() {
59
+ const fetchStartDate = new Date();
60
+ debug('Starting email analytics fetch of latest events');
61
+ const eventStats = await this.service.fetchLatest();
62
+ const fetchEndDate = new Date();
63
+ debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms`);
64
+
65
+ const aggregateStartDate = new Date();
66
+ debug(`Starting email analytics aggregation for ${eventStats.emailIds.length} emails`);
67
+ await this.service.aggregateStats(eventStats);
68
+ const aggregateEndDate = new Date();
69
+ debug(`Finished aggregating email analytics in ${aggregateEndDate.getTime() - aggregateStartDate.getTime()}ms`);
70
+
71
+ logging.info(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate.getTime() - fetchStartDate.getTime()}ms`);
72
+
73
+ return eventStats;
74
+ }
75
+
76
+ async startFetch() {
77
+ if (this.fetching) {
78
+ logging.info('Email analytics fetch already running, skipping');
79
+ return;
80
+ }
81
+ this.fetching = true;
82
+
83
+ logging.info('Email analytics fetch started');
84
+ try {
85
+ const eventStats = await this.fetchLatest();
86
+
87
+ this.fetching = false;
88
+ return eventStats;
89
+ } catch (e) {
90
+ logging.error(e, 'Error while fetching email analytics');
91
+
92
+ // Log again only the error, otherwise we lose the stack trace
93
+ logging.error(e);
94
+ }
95
+ this.fetching = false;
96
+ }
97
+ }
98
+
99
+ module.exports = EmailAnalyticsServiceWrapper;
@@ -13,8 +13,8 @@ class EmailServiceWrapper {
13
13
  return;
14
14
  }
15
15
 
16
- const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, EmailEventStorage, MailgunEmailProvider} = require('@tryghost/email-service');
17
- const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member, EmailRecipientFailure, EmailSpamComplaintEvent} = require('../../models');
16
+ const {EmailService, EmailController, EmailRenderer, SendingService, BatchSendingService, EmailSegmenter, MailgunEmailProvider} = require('@tryghost/email-service');
17
+ const {Post, Newsletter, Email, EmailBatch, EmailRecipient, Member} = require('../../models');
18
18
  const MailgunClient = require('@tryghost/mailgun-client');
19
19
  const configService = require('../../../shared/config');
20
20
  const settingsCache = require('../../../shared/settings-cache');
@@ -25,7 +25,6 @@ class EmailServiceWrapper {
25
25
  const sentry = require('../../../shared/sentry');
26
26
  const membersRepository = membersService.api.members;
27
27
  const limitService = require('../limits');
28
- const domainEvents = require('@tryghost/domain-events');
29
28
 
30
29
  const mobiledocLib = require('../../lib/mobiledoc');
31
30
  const lexicalLib = require('../../lib/lexical');
@@ -59,7 +58,7 @@ class EmailServiceWrapper {
59
58
  settingsHelpers,
60
59
  renderers: {
61
60
  mobiledoc: mobiledocLib.mobiledocHtmlRenderer,
62
- lexical: lexicalLib.lexicalHtmlRenderer
61
+ lexical: lexicalLib
63
62
  },
64
63
  imageSize,
65
64
  urlUtils,
@@ -116,16 +115,6 @@ class EmailServiceWrapper {
116
115
  Email
117
116
  }
118
117
  });
119
-
120
- this.eventStorage = new EmailEventStorage({
121
- db,
122
- membersRepository,
123
- models: {
124
- EmailRecipientFailure,
125
- EmailSpamComplaintEvent
126
- }
127
- });
128
- this.eventStorage.listen(domainEvents);
129
118
  }
130
119
  }
131
120
 
@@ -303,7 +303,7 @@ const PostEmailSerializer = {
303
303
  }
304
304
 
305
305
  if (post.lexical) {
306
- post.html = lexicalLib.lexicalHtmlRenderer.render(
306
+ post.html = lexicalLib.render(
307
307
  post.lexical, {target: 'email', postUrl: post.url}
308
308
  );
309
309
  } else {
@@ -14,10 +14,12 @@ module.exports = {
14
14
  ) {
15
15
  // use a random seconds value to avoid spikes to external APIs on the minute
16
16
  const s = Math.floor(Math.random() * 60); // 0-59
17
+ const m = Math.floor(Math.random() * 60); // 0-59
18
+ const h = Math.floor(Math.random() * 6); // 0-5
17
19
 
18
- // Run everyday at 12:05:X AM to clean all expired complimentary subscriptions
20
+ // Run everyday at {0-5}:XX:XX AM to clean all expired complimentary subscriptions
19
21
  jobsService.addJob({
20
- at: `${s} 5 0 * * *`,
22
+ at: `${s} ${m} ${h} * * *`,
21
23
  job: path.resolve(__dirname, 'clean-expired-comped.js'),
22
24
  name: 'clean-expired-comped'
23
25
  });
@@ -54,7 +54,7 @@ module.exports = class BookshelfMentionRepository {
54
54
  sourceAuthor: model.get('source_author'),
55
55
  sourceExcerpt: model.get('source_excerpt'),
56
56
  sourceFavicon: model.get('source_favicon'),
57
- sourceFeaturedImaged: model.get('source_featured_image')
57
+ sourceFeaturedImage: model.get('source_featured_image')
58
58
  });
59
59
  }
60
60
 
@@ -106,7 +106,8 @@ module.exports = class BookshelfMentionRepository {
106
106
  target: mention.target.href,
107
107
  resource_id: mention.resourceId?.toHexString(),
108
108
  resource_type: mention.resourceId ? 'post' : null,
109
- payload: mention.payload ? JSON.stringify(mention.payload) : null
109
+ payload: mention.payload ? JSON.stringify(mention.payload) : null,
110
+ deleted: Mention.isDeleted(mention)
110
111
  };
111
112
 
112
113
  const existing = await this.#MentionModel.findOne({id: data.id}, {require: false});
@@ -10,17 +10,53 @@ const logging = require('@tryghost/logging');
10
10
  * @typedef {import('@tryghost/webmentions/lib/MentionsAPI').Page} Page<Model>
11
11
  */
12
12
 
13
+ /**
14
+ * @typedef {object} MentionResource
15
+ * @prop {ObjectID} id
16
+ * @prop {string} type
17
+ * @prop {string} name
18
+ */
19
+
20
+ /**
21
+ * @typedef {Mention} MentionDTO
22
+ * @prop {Resource} resource
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} IJobService
27
+ * @prop {(name: string, fn: Function) => void} addJob
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} IMentionResourceService
32
+ * @prop {(id: ObjectID) => Promise<MentionResource>} getByID
33
+ */
34
+
13
35
  module.exports = class MentionController {
14
36
  /** @type {import('@tryghost/webmentions/lib/MentionsAPI')} */
15
37
  #api;
16
38
 
39
+ /** @type {IJobService} */
40
+ #jobService;
41
+
42
+ /** @type {IMentionResourceService} */
43
+ #mentionResourceService;
44
+
45
+ /**
46
+ * @param {object} deps
47
+ * @param {import('@tryghost/webmentions/lib/MentionsAPI')} deps.api
48
+ * @param {IJobService} deps.jobService
49
+ * @param {IMentionResourceService} deps.mentionResourceService
50
+ */
17
51
  async init(deps) {
18
52
  this.#api = deps.api;
53
+ this.#jobService = deps.jobService;
54
+ this.#mentionResourceService = deps.mentionResourceService;
19
55
  }
20
56
 
21
57
  /**
22
58
  * @param {import('@tryghost/api-framework').Frame} frame
23
- * @returns {Promise<Page<Mention>>}
59
+ * @returns {Promise<Page<MentionDTO>>}
24
60
  */
25
61
  async browse(frame) {
26
62
  let limit;
@@ -37,13 +73,41 @@ module.exports = class MentionController {
37
73
  page = 1;
38
74
  }
39
75
 
40
- const results = await this.#api.listMentions({
76
+ let order;
77
+ if (frame.options.order && frame.options.order === 'created_at desc') {
78
+ order = 'created_at desc';
79
+ } else {
80
+ order = 'created_at asc';
81
+ }
82
+
83
+ const mentions = await this.#api.listMentions({
41
84
  filter: frame.options.filter,
85
+ order,
42
86
  limit,
43
87
  page
44
88
  });
45
89
 
46
- return results;
90
+ const resources = await Promise.all(mentions.data.map((mention) => {
91
+ return this.#mentionResourceService.getByID(mention.resourceId);
92
+ }));
93
+
94
+ /** @type {Page<MentionDTO>} */
95
+ const result = {
96
+ data: mentions.data.map((mention, index) => {
97
+ const mentionDTO = {
98
+ ...mention.toJSON(),
99
+ resource: resources[index],
100
+ toJSON() {
101
+ return mentionDTO;
102
+ }
103
+ };
104
+ delete mentionDTO.resourceId;
105
+ return mentionDTO;
106
+ }),
107
+ meta: mentions.meta
108
+ };
109
+
110
+ return result;
47
111
  }
48
112
 
49
113
  /**
@@ -52,15 +116,17 @@ module.exports = class MentionController {
52
116
  */
53
117
  async receive(frame) {
54
118
  logging.info('[Webmention] ' + JSON.stringify(frame.data));
55
- const {source, target, ...payload} = frame.data;
56
- const result = this.#api.processWebmention({
57
- source: new URL(source),
58
- target: new URL(target),
59
- payload
60
- });
61
-
62
- result.catch(function rejected(err) {
63
- logging.error(err);
119
+ this.#jobService.addJob('processWebmention', async () => {
120
+ const {source, target, ...payload} = frame.data;
121
+ try {
122
+ await this.#api.processWebmention({
123
+ source: new URL(source),
124
+ target: new URL(target),
125
+ payload
126
+ });
127
+ } catch (err) {
128
+ logging.error(err);
129
+ }
64
130
  });
65
131
  }
66
132
  };
@@ -13,9 +13,10 @@ const events = require('../../lib/common/events');
13
13
  const externalRequest = require('../../../server/lib/request-external.js');
14
14
  const urlUtils = require('../../../shared/url-utils');
15
15
  const outputSerializerUrlUtil = require('../../../server/api/endpoints/utils/serializers/output/utils/url');
16
- const labs = require('../../../shared/labs');
17
16
  const urlService = require('../url');
17
+ const settingsCache = require('../../../shared/settings-cache');
18
18
  const DomainEvents = require('@tryghost/domain-events');
19
+ const jobsService = require('../jobs');
19
20
 
20
21
  function getPostUrl(post) {
21
22
  const jsonModel = {};
@@ -50,14 +51,53 @@ module.exports = {
50
51
  routingService
51
52
  });
52
53
 
53
- this.controller.init({api});
54
+ this.controller.init({
55
+ api,
56
+ jobService: {
57
+ async addJob(name, fn) {
58
+ jobsService.addJob({
59
+ name,
60
+ job: fn,
61
+ offloaded: false
62
+ });
63
+ }
64
+ },
65
+ mentionResourceService: {
66
+ async getByID(id) {
67
+ if (!id) {
68
+ return null;
69
+ }
70
+
71
+ const post = await models.Post.findOne({id: id.toHexString()});
72
+
73
+ if (!post) {
74
+ return null;
75
+ }
76
+
77
+ return {
78
+ id: id,
79
+ name: post.get('title'),
80
+ type: 'post'
81
+ };
82
+ }
83
+ }
84
+ });
54
85
 
55
86
  const sendingService = new MentionSendingService({
56
87
  discoveryService,
57
88
  externalRequest,
58
89
  getSiteUrl: () => urlUtils.urlFor('home', true),
59
90
  getPostUrl: post => getPostUrl(post),
60
- isEnabled: () => labs.isSet('webmentions')
91
+ isEnabled: () => !settingsCache.get('is_private'),
92
+ jobService: {
93
+ async addJob(name, fn) {
94
+ jobsService.addJob({
95
+ name,
96
+ job: fn,
97
+ offloaded: false
98
+ });
99
+ }
100
+ }
61
101
  });
62
102
  sendingService.listen(events);
63
103
  }
@@ -0,0 +1,58 @@
1
+ const MIN_DAYS_SINCE_IMPORTED = 7;
2
+
3
+ module.exports = class MilestoneQueries {
4
+ #db;
5
+
6
+ constructor(deps) {
7
+ this.#db = deps.db;
8
+ }
9
+
10
+ /**
11
+ * @returns {Promise<number>}
12
+ */
13
+ async getMembersCount() {
14
+ const [membersCount] = await this.#db.knex('members').count('id as count');
15
+
16
+ return membersCount?.count || 0;
17
+ }
18
+
19
+ /**
20
+ * @returns {Promise<Array>}
21
+ */
22
+ async getARR() {
23
+ const currentARR = await this.#db.knex('members_paid_subscription_events as stripe')
24
+ .select(this.#db.knex.raw('ROUND(SUM(stripe.mrr_delta) * 12) / 100 AS arr, stripe.currency as currency'))
25
+ .groupBy('stripe.currency');
26
+
27
+ return currentARR;
28
+ }
29
+
30
+ /**
31
+ * @returns {Promise<boolean>}
32
+ */
33
+ async hasImportedMembersInPeriod() {
34
+ const [hasImportedMembers] = await this.#db.knex('members_subscribe_events')
35
+ .count('id as count')
36
+ .where('source', '=', 'import')
37
+ .where('created_at', '>=', MIN_DAYS_SINCE_IMPORTED);
38
+
39
+ return hasImportedMembers?.count > 0;
40
+ }
41
+
42
+ /**
43
+ * @returns {Promise<string>}
44
+ */
45
+ async getDefaultCurrency() {
46
+ const currentARR = await this.getARR();
47
+
48
+ // Set the default currency as the one with the highest value
49
+ if (currentARR.length > 1) {
50
+ const highestValues = currentARR.sort((a, b) => b.arr - a.arr);
51
+ return highestValues?.[0]?.currency;
52
+ } else if (currentARR?.[0]?.currency) {
53
+ return currentARR[0].currency;
54
+ } else {
55
+ return 'usd';
56
+ }
57
+ }
58
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./service');
@@ -0,0 +1,58 @@
1
+ // Stubbing stripe in test was causing issues. Moved it
2
+ // into this function to be able to rewire and stub the
3
+ // expected return value.
4
+ const getStripeLiveEnabled = () => {
5
+ const stripeService = require('../stripe');
6
+ // This seems to be the only true way to check if Stripe is configured in live mode
7
+ // settingsCache only cares if Stripe is enabled
8
+ return stripeService.api.configured && stripeService.api.mode === 'live';
9
+ };
10
+
11
+ /**
12
+ *
13
+ * @returns {Promise<any>}
14
+ */
15
+ module.exports = {
16
+ async initAndRun() {
17
+ const labs = require('../../../shared/labs');
18
+
19
+ if (labs.isSet('milestoneEmails')) {
20
+ const db = require('../../data/db');
21
+ const MilestoneQueries = require('./MilestoneQueries');
22
+
23
+ const {
24
+ MilestonesEmailService,
25
+ InMemoryMilestoneRepository
26
+ } = require('@tryghost/milestone-emails');
27
+ const config = require('../../../shared/config');
28
+ const milestonesConfig = config.get('milestones');
29
+ const {GhostMailer} = require('../mail');
30
+
31
+ const mailer = new GhostMailer();
32
+ const repository = new InMemoryMilestoneRepository();
33
+ const queries = new MilestoneQueries({db});
34
+
35
+ const milestonesEmailService = new MilestonesEmailService({
36
+ mailer,
37
+ repository,
38
+ milestonesConfig, // avoid using getters and pass as JSON
39
+ queries
40
+ });
41
+
42
+ let arrResult;
43
+
44
+ // @TODO: schedule recurring jobs instead
45
+ const membersResult = await milestonesEmailService.checkMilestones('members');
46
+ const stripeLiveEnabled = getStripeLiveEnabled();
47
+
48
+ if (stripeLiveEnabled) {
49
+ arrResult = await milestonesEmailService.checkMilestones('arr');
50
+ }
51
+
52
+ return {
53
+ members: membersResult,
54
+ arr: arrResult
55
+ };
56
+ }
57
+ }
58
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('./service');
@@ -0,0 +1,31 @@
1
+ class TagsPublicServiceWrapper {
2
+ async init() {
3
+ if (this.api) {
4
+ // Already done
5
+ return;
6
+ }
7
+
8
+ // Wire up all the dependencies
9
+ const models = require('../../models');
10
+ const adapterManager = require('../adapter-manager');
11
+ const config = require('../../../shared/config');
12
+
13
+ let tagsCache;
14
+ if (config.get('hostSettings:tagsPublicCache:enabled')) {
15
+ tagsCache = adapterManager.getAdapter('cache:tagsPublic');
16
+ }
17
+
18
+ const {TagsPublicRepository} = require('@tryghost/tags-public');
19
+
20
+ this.linkRedirectRepository = new TagsPublicRepository({
21
+ Tag: models.TagPublic,
22
+ cache: tagsCache
23
+ });
24
+
25
+ this.api = {
26
+ browse: this.linkRedirectRepository.getAll.bind(this.linkRedirectRepository)
27
+ };
28
+ }
29
+ }
30
+
31
+ module.exports = new TagsPublicServiceWrapper();
@@ -240,7 +240,6 @@ module.exports = function apiRoutes() {
240
240
  mw.authAdminApi,
241
241
  apiMw.upload.single('file'),
242
242
  apiMw.upload.validation({type: 'images'}),
243
- apiMw.normalizeImage,
244
243
  http(api.images.upload)
245
244
  );
246
245
 
@@ -1,6 +1,5 @@
1
1
  module.exports = {
2
2
  cors: require('./cors'),
3
- normalizeImage: require('./normalize-image'),
4
3
  updateUserLastSeen: require('./update-user-last-seen'),
5
4
  upload: require('./upload'),
6
5
  versionMatch: require('./version-match')
@@ -20,7 +20,8 @@ const messages = {
20
20
  error: 'Only {rateSigninAttempts} tries per IP address every {rateSigninPeriod} seconds.',
21
21
  context: 'Too many login attempts.'
22
22
  },
23
- tooManyAttempts: 'Too many attempts.'
23
+ tooManyAttempts: 'Too many attempts.',
24
+ webmentionsBlock: 'Too many mention attempts'
24
25
  };
25
26
  let spamPrivateBlock = spam.private_block || {};
26
27
  let spamGlobalBlock = spam.global_block || {};
@@ -29,12 +30,14 @@ let spamUserReset = spam.user_reset || {};
29
30
  let spamUserLogin = spam.user_login || {};
30
31
  let spamMemberLogin = spam.member_login || {};
31
32
  let spamContentApiKey = spam.content_api_key || {};
33
+ let spamWebmentionsBlock = spam.webmentions_block || {};
32
34
 
33
35
  let store;
34
36
  let memoryStore;
35
37
  let privateBlogInstance;
36
38
  let globalResetInstance;
37
39
  let globalBlockInstance;
40
+ let webmentionsBlockInstance;
38
41
  let userLoginInstance;
39
42
  let membersAuthInstance;
40
43
  let membersAuthEnumerationInstance;
@@ -123,6 +126,32 @@ const globalReset = () => {
123
126
  return globalResetInstance;
124
127
  };
125
128
 
129
+ const webmentionsBlock = () => {
130
+ const ExpressBrute = require('express-brute');
131
+ const BruteKnex = require('brute-knex');
132
+ const db = require('../../../../data/db');
133
+
134
+ store = store || new BruteKnex({
135
+ tablename: 'brute',
136
+ createTable: false,
137
+ knex: db.knex
138
+ });
139
+
140
+ webmentionsBlockInstance = webmentionsBlockInstance || new ExpressBrute(store,
141
+ extend({
142
+ attachResetToRequest: false,
143
+ failCallback(req, res, next) {
144
+ return next(new errors.TooManyRequestsError({
145
+ message: messages.webmentionsBlock
146
+ }));
147
+ },
148
+ handleStoreError: handleStoreError
149
+ }, pick(spamWebmentionsBlock, spamConfigKeys))
150
+ );
151
+
152
+ return webmentionsBlockInstance;
153
+ };
154
+
126
155
  const membersAuth = () => {
127
156
  const ExpressBrute = require('express-brute');
128
157
  const BruteKnex = require('brute-knex');
@@ -319,6 +348,7 @@ module.exports = {
319
348
  userReset: userReset,
320
349
  privateBlog: privateBlog,
321
350
  contentApiKey: contentApiKey,
351
+ webmentionsBlock: webmentionsBlock,
322
352
  reset: () => {
323
353
  store = undefined;
324
354
  memoryStore = undefined;
@@ -104,5 +104,18 @@ module.exports = {
104
104
  */
105
105
  membersAuthEnumeration(req, res, next) {
106
106
  return spamPrevention.membersAuthEnumeration().prevent(req, res, next);
107
+ },
108
+
109
+ /**
110
+ * Blocks webmention spam
111
+ */
112
+
113
+ webmentionsLimiter(req, res, next) {
114
+ return spamPrevention.webmentionsBlock().getMiddleware({
115
+ ignoreIP: false,
116
+ key(_req, _res, _next) {
117
+ return _next('webmention_blocked');
118
+ }
119
+ })(req, res, next);
107
120
  }
108
121
  };