ghost 5.3.1 → 5.5.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 (107) hide show
  1. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  2. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  3. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  4. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  5. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  6. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  7. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  8. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  9. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  10. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  11. package/components/tryghost-members-csv-0.0.0.tgz +0 -0
  12. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  13. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  14. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  15. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  16. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  17. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  18. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  19. package/content/themes/casper/assets/built/global.css +1 -1
  20. package/content/themes/casper/assets/built/global.css.map +1 -1
  21. package/content/themes/casper/assets/built/screen.css +1 -1
  22. package/content/themes/casper/assets/built/screen.css.map +1 -1
  23. package/content/themes/casper/assets/css/screen.css +31 -8
  24. package/content/themes/casper/default.hbs +8 -5
  25. package/content/themes/casper/gulpfile.js +1 -1
  26. package/content/themes/casper/package.json +9 -9
  27. package/content/themes/casper/yarn.lock +1154 -1249
  28. package/core/boot.js +5 -0
  29. package/core/built/assets/{chunk.3.dc389a0f93cb5fabd695.js → chunk.3.550552fbc71864fb9738.js} +20 -20
  30. package/core/built/assets/fonts/Inter.ttf +0 -0
  31. package/core/built/assets/ghost-dark-5c2a961b35311d7298136e02289d98b2.css +1 -0
  32. package/core/built/assets/ghost.min-a89d10b3b58c1a5ebaca68cef93a404c.css +1 -0
  33. package/core/built/assets/{ghost.min-f4bba3a2a5ef256b82641345505d4f0f.js → ghost.min-c75f224decd20f9538179d7564cd2ab4.js} +3025 -2883
  34. package/core/built/assets/icons/event-comment.svg +3 -0
  35. package/core/built/assets/{vendor.min-4076498ccd6c8412365f43b156084ed8.js → vendor.min-cf3af99dca0c71937669305afb3686a1.js} +6122 -3197
  36. package/core/frontend/helpers/comments.js +22 -10
  37. package/core/frontend/helpers/ghost_head.js +22 -4
  38. package/core/frontend/helpers/total_members.js +17 -0
  39. package/core/frontend/helpers/total_paid_members.js +16 -0
  40. package/core/frontend/utils/frontend-apps.js +33 -0
  41. package/core/frontend/utils/member-count.js +50 -0
  42. package/core/frontend/web/middleware/cors.js +2 -1
  43. package/core/server/api/endpoints/comments-comments.js +50 -32
  44. package/core/server/api/endpoints/offers-public.js +2 -2
  45. package/core/server/api/endpoints/offers.js +8 -8
  46. package/core/server/api/endpoints/settings.js +62 -30
  47. package/core/server/api/endpoints/utils/serializers/input/settings.js +1 -0
  48. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  49. package/core/server/api/endpoints/utils/serializers/output/index.js +0 -4
  50. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
  51. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
  52. package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +2 -0
  53. package/core/server/api/endpoints/utils/serializers/output/mappers/offers.js +28 -0
  54. package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
  55. package/core/server/api/endpoints/utils/serializers/output/settings.js +2 -1
  56. package/core/server/api/endpoints/utils/validators/input/settings.js +22 -2
  57. package/core/server/data/exporter/table-lists.js +2 -1
  58. package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
  59. package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
  60. package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
  61. package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
  62. package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
  63. package/core/server/data/schema/commands.js +7 -2
  64. package/core/server/data/schema/fixtures/fixtures.json +5 -0
  65. package/core/server/data/schema/schema.js +12 -4
  66. package/core/server/ghost-server.js +0 -22
  67. package/core/server/models/comment-report.js +34 -0
  68. package/core/server/models/comment.js +8 -7
  69. package/core/server/models/job.js +9 -0
  70. package/core/server/models/tag.js +4 -0
  71. package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
  72. package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
  73. package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
  74. package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
  75. package/core/server/services/comments/email-templates/report.hbs +199 -0
  76. package/core/server/services/comments/email-templates/report.txt.js +16 -0
  77. package/core/server/services/comments/emails.js +57 -1
  78. package/core/server/services/comments/service.js +194 -2
  79. package/core/server/services/jobs/job-service.js +24 -1
  80. package/core/server/services/mail/GhostMailer.js +1 -0
  81. package/core/server/services/members/SingleUseTokenProvider.js +3 -3
  82. package/core/server/services/members/api.js +2 -1
  83. package/core/server/services/members/config.js +4 -1
  84. package/core/server/services/members/middleware.js +14 -2
  85. package/core/server/services/members/settings.js +4 -90
  86. package/core/server/services/public-config/config.js +2 -1
  87. package/core/server/services/settings/emails/verify-email.js +166 -0
  88. package/core/server/services/settings/settings-bread-service.js +170 -4
  89. package/core/server/services/settings/settings-service.js +9 -1
  90. package/core/server/services/stripe/service.js +9 -1
  91. package/core/server/services/webhooks/serialize.js +5 -0
  92. package/core/server/web/admin/views/default-prod.html +4 -4
  93. package/core/server/web/admin/views/default.html +4 -4
  94. package/core/server/web/api/endpoints/admin/routes.js +6 -0
  95. package/core/server/web/api/endpoints/content/routes.js +2 -1
  96. package/core/server/web/api/middleware/cors.js +2 -1
  97. package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
  98. package/core/server/web/api/testmode/routes.js +14 -0
  99. package/core/server/web/comments/routes.js +2 -0
  100. package/core/server/web/members/app.js +2 -4
  101. package/core/shared/config/defaults.json +15 -7
  102. package/core/shared/config/env/config.testing.json +3 -2
  103. package/package.json +75 -60
  104. package/yarn.lock +1812 -1832
  105. package/core/built/assets/ghost-dark-9e5d1f0dfae41232e5e34e4d0df53ae0.css +0 -1
  106. package/core/built/assets/ghost.min-e7cfbd1800f8e99b9158f74f1e39cd76.css +0 -1
  107. package/core/server/api/endpoints/utils/serializers/output/offers.js +0 -16
@@ -0,0 +1,199 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width">
5
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
6
+ <title>🚩 A comment has been reported on {{postTitle}}</title>
7
+ <style>
8
+ /* -------------------------------------
9
+ RESPONSIVE AND MOBILE FRIENDLY STYLES
10
+ ------------------------------------- */
11
+ @media only screen and (max-width: 620px) {
12
+ table[class=body] h1 {
13
+ font-size: 28px !important;
14
+ margin-bottom: 10px !important;
15
+ }
16
+ table[class=body] p,
17
+ table[class=body] ul,
18
+ table[class=body] ol,
19
+ table[class=body] td,
20
+ table[class=body] span,
21
+ table[class=body] a {
22
+ font-size: 16px !important;
23
+ }
24
+ table[class=body] .wrapper,
25
+ table[class=body] .article {
26
+ padding: 10px !important;
27
+ }
28
+ table[class=body] .content {
29
+ padding: 0 !important;
30
+ }
31
+ table[class=body] .container {
32
+ padding: 0 !important;
33
+ width: 100% !important;
34
+ }
35
+ table[class=body] .main {
36
+ border-left-width: 0 !important;
37
+ border-radius: 0 !important;
38
+ border-right-width: 0 !important;
39
+ }
40
+ table[class=body] .btn table {
41
+ width: 100% !important;
42
+ }
43
+ table[class=body] .btn a {
44
+ width: 100% !important;
45
+ }
46
+ table[class=body] .img-responsive {
47
+ height: auto !important;
48
+ max-width: 100% !important;
49
+ width: auto !important;
50
+ }
51
+ table[class=body] p[class=small],
52
+ table[class=body] a[class=small] {
53
+ font-size: 11px !important;
54
+ }
55
+ }
56
+ /* -------------------------------------
57
+ PRESERVE THESE STYLES IN THE HEAD
58
+ ------------------------------------- */
59
+ @media all {
60
+ .ExternalClass {
61
+ width: 100%;
62
+ }
63
+ .ExternalClass,
64
+ .ExternalClass p,
65
+ .ExternalClass span,
66
+ .ExternalClass font,
67
+ .ExternalClass td,
68
+ .ExternalClass div {
69
+ line-height: 100%;
70
+ }
71
+ .recipient-link a {
72
+ color: inherit !important;
73
+ font-family: inherit !important;
74
+ font-size: inherit !important;
75
+ font-weight: inherit !important;
76
+ line-height: inherit !important;
77
+ text-decoration: none !important;
78
+ }
79
+ #MessageViewBody a {
80
+ color: inherit;
81
+ text-decoration: none;
82
+ font-size: inherit;
83
+ font-family: inherit;
84
+ font-weight: inherit;
85
+ line-height: inherit;
86
+ }
87
+ }
88
+ hr {
89
+ border-width: 0;
90
+ height: 0;
91
+ margin-top: 34px;
92
+ margin-bottom: 34px;
93
+ border-bottom-width: 1px;
94
+ border-bottom-color: #EEF5F8;
95
+ }
96
+ a {
97
+ color: #3A464C;
98
+ }
99
+ </style>
100
+ </head>
101
+ <body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
102
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
103
+ <tr>
104
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
105
+ <td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
106
+ <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
107
+
108
+ <!-- START CENTERED CONTAINER -->
109
+ <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">{{reporterName}} ({{reporterEmail}}) reported a comment on your post</span>
110
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
111
+
112
+ <!-- START MAIN CONTENT AREA -->
113
+ <tr>
114
+ <td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
115
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
116
+ <tr>
117
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
118
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
119
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">{{reporter}} has reported the comment below on <a href="{{postUrl}}" target="_blank">{{postTitle}}</a>. This comment will remain visible until you choose to remove it, which can be done directly on the post.</p>
120
+
121
+ <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
122
+ <tbody>
123
+ <tr>
124
+ <td align="left" style="padding: 16px;">
125
+ <table border="0" cellpadding="0" cellspacing="0">
126
+ <tr>
127
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px;">
128
+ <table border="0" cellpadding="0" cellspacing="0" width="44" height="44" style="width: 44px; height: 44px; background-color: #15171A; border-radius: 999px;">
129
+ <tr>
130
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 18px; color: #FFFFFF; text-align: center; font-weight: 500; height: 44px; width: 44px;">{{memberInitials}}</td>
131
+ </tr>
132
+ </table>
133
+ </td>
134
+ <td style="padding-right: 8px;">
135
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberName}}</p>
136
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">{{#if memberBio}}{{memberBio}} &#8226; {{/if}}{{commentDate}}</p>
137
+ </td>
138
+ </tr>
139
+ </table>
140
+ </td>
141
+ </tr>
142
+ <tr>
143
+ <td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px; color: #15171A;">
144
+ {{{commentHtml}}}
145
+ </td>
146
+ </tr>
147
+ </tbody>
148
+ </table>
149
+
150
+ <table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
151
+ <tbody>
152
+ <tr>
153
+ <td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
154
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
155
+ <tbody>
156
+ <tr>
157
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: {{accentColor}}; border-radius: 5px; text-align: center;"> <a href="{{postUrl}}#ghost-comments-root" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: {{accentColor}};">View comment</a> </td>
158
+ </tr>
159
+ </tbody>
160
+ </table>
161
+ </td>
162
+ </tr>
163
+ </tbody>
164
+ </table>
165
+ <hr/>
166
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
167
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;">{{postUrl}}#ghost-comments-root</p>
168
+ </td>
169
+ </tr>
170
+
171
+ <!-- START FOOTER -->
172
+ <tr>
173
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
174
+ <p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{toEmail}}</a></p>
175
+ </td>
176
+ </tr>
177
+ <tr>
178
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px">
179
+ <p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;"><a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">Manage your email preferences</a></p>
180
+ </td>
181
+ </tr>
182
+
183
+ <!-- END FOOTER -->
184
+ </table>
185
+ </td>
186
+ </tr>
187
+
188
+ <!-- END MAIN CONTENT AREA -->
189
+ </table>
190
+
191
+
192
+ <!-- END CENTERED CONTAINER -->
193
+ </div>
194
+ </td>
195
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
196
+ </tr>
197
+ </table>
198
+ </body>
199
+ </html>
@@ -0,0 +1,16 @@
1
+ module.exports = function (data) {
2
+ // Be careful when you indent the email, because whitespaces are visible in emails!
3
+ return `Hey there,
4
+
5
+ ${data.reporter} has reported the comment below on ${data.postTitle}. This comment will remain visible until you choose to remove it, which can be done directly on the post.
6
+
7
+ ${data.memberName} (${data.memberEmail}):
8
+ ${data.commentText}
9
+
10
+ ${data.postUrl}#ghost-comments-root
11
+
12
+ ---
13
+
14
+ Sent to ${data.toEmail} from ${data.siteDomain}.
15
+ You can manage your notification preferences at ${data.staffUrl}.`;
16
+ };
@@ -1,6 +1,7 @@
1
1
  const {promises: fs} = require('fs');
2
2
  const path = require('path');
3
3
  const moment = require('moment');
4
+ const htmlToPlaintext = require('../../../shared/html-to-plaintext');
4
5
 
5
6
  class CommentsServiceEmails {
6
7
  constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils}) {
@@ -48,7 +49,7 @@ class CommentsServiceEmails {
48
49
 
49
50
  const {html, text} = await this.renderEmailTemplate('new-comment', templateData);
50
51
 
51
- this.sendMail({
52
+ await this.sendMail({
52
53
  to,
53
54
  subject,
54
55
  html,
@@ -65,6 +66,11 @@ class CommentsServiceEmails {
65
66
  return;
66
67
  }
67
68
 
69
+ // Don't send a notification if you reply to your own comment
70
+ if (parentMember.id === reply.get('member_id')) {
71
+ return;
72
+ }
73
+
68
74
  const to = parentMember.get('email');
69
75
  const subject = '💬 You have a new reply on one of your comments';
70
76
 
@@ -100,6 +106,56 @@ class CommentsServiceEmails {
100
106
  });
101
107
  }
102
108
 
109
+ /**
110
+ * Send an email to notify the owner of the site that a comment has been reported by a member
111
+ * @param {*} comment The comment model that has been reported
112
+ * @param {*} reporter The member object who reported this comment
113
+ */
114
+ async notifiyReport(comment, reporter) {
115
+ const post = await this.models.Post.findOne({id: comment.get('post_id')}, {withRelated: ['authors']});
116
+ const member = await this.models.Member.findOne({id: comment.get('member_id')});
117
+ const owner = await this.models.User.getOwnerUser();
118
+
119
+ // For now we only send the report to the owner
120
+ const to = owner.get('email');
121
+ const subject = '🚩 A comment has been reported on your post';
122
+
123
+ const memberName = member.get('name') || 'Anonymous';
124
+
125
+ const templateData = {
126
+ siteTitle: this.settingsCache.get('title'),
127
+ siteUrl: this.urlUtils.getSiteUrl(),
128
+ siteDomain: this.siteDomain,
129
+ postTitle: post.get('title'),
130
+ postUrl: this.urlService.getUrlByResourceId(post.get('id'), {absolute: true}),
131
+ commentHtml: comment.get('html'),
132
+ commentText: htmlToPlaintext.email(comment.get('html')),
133
+ commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
134
+
135
+ reporterName: reporter.name,
136
+ reporterEmail: reporter.email,
137
+ reporter: reporter.name ? `${reporter.name} (${reporter.email})` : reporter.email,
138
+
139
+ memberName: memberName,
140
+ memberEmail: member.get('email'),
141
+ memberBio: member.get('bio'),
142
+ memberInitials: this.extractInitials(memberName),
143
+ accentColor: this.settingsCache.get('accent_color'),
144
+ fromEmail: this.notificationFromAddress,
145
+ toEmail: to,
146
+ staffUrl: `${this.urlUtils.getAdminUrl()}ghost/#/settings/staff/${owner.get('slug')}`
147
+ };
148
+
149
+ const {html, text} = await this.renderEmailTemplate('report', templateData);
150
+
151
+ await this.sendMail({
152
+ to,
153
+ subject,
154
+ html,
155
+ text
156
+ });
157
+ }
158
+
103
159
  // Utils
104
160
 
105
161
  get siteDomain() {
@@ -1,3 +1,16 @@
1
+ const tpl = require('@tryghost/tpl');
2
+ const errors = require('@tryghost/errors');
3
+ const {MemberCommentEvent} = require('@tryghost/member-events');
4
+ const DomainEvents = require('@tryghost/domain-events');
5
+
6
+ const messages = {
7
+ commentNotFound: 'Comment could not be found',
8
+ memberNotFound: 'Unable to find member',
9
+ likeNotFound: 'Unable to find like',
10
+ alreadyLiked: 'This comment was liked already',
11
+ replyToReply: 'Can not reply to a reply'
12
+ };
13
+
1
14
  class CommentsService {
2
15
  constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils}) {
3
16
  this.config = config;
@@ -13,11 +26,190 @@ class CommentsService {
13
26
  }
14
27
 
15
28
  async sendNewCommentNotifications(comment) {
16
- this.emails.notifyPostAuthors(comment);
29
+ await this.emails.notifyPostAuthors(comment);
17
30
 
18
31
  if (comment.get('parent_id')) {
19
- this.emails.notifyParentCommentAuthor(comment);
32
+ await this.emails.notifyParentCommentAuthor(comment);
33
+ }
34
+ }
35
+
36
+ async reportComment(commentId, reporter) {
37
+ const comment = await this.models.Comment.findOne({id: commentId}, {require: true});
38
+
39
+ // Check if this reporter already reported this comment (then don't send an email)?
40
+ const existing = await this.models.CommentReport.findOne({
41
+ comment_id: comment.id,
42
+ member_id: reporter.id
43
+ });
44
+
45
+ if (existing) {
46
+ // Ignore silently for now
47
+ return;
48
+ }
49
+
50
+ // Save report model
51
+ await this.models.CommentReport.add({
52
+ comment_id: comment.id,
53
+ member_id: reporter.id
54
+ });
55
+
56
+ await this.emails.notifiyReport(comment, reporter);
57
+ }
58
+
59
+ /**
60
+ * @param {string} id - The ID of the Comment to get
61
+ * @param {any} options
62
+ */
63
+ async getCommentByID(id, options) {
64
+ const model = await this.models.Comment.findOne({id}, options);
65
+
66
+ if (!model) {
67
+ throw new errors.NotFoundError({
68
+ messages: tpl(messages.commentNotFound)
69
+ });
70
+ }
71
+
72
+ return model;
73
+ }
74
+
75
+ /**
76
+ * @param {string} post - The ID of the Post to comment on
77
+ * @param {string} member - The ID of the Member to comment as
78
+ * @param {string} comment - The HTML content of the Comment
79
+ * @param {any} options
80
+ */
81
+ async commentOnPost(post, member, comment, options) {
82
+ await this.models.Member.findOne({
83
+ id: member
84
+ }, {
85
+ require: true,
86
+ ...options
87
+ });
88
+
89
+ const model = await this.models.Comment.add({
90
+ post_id: post,
91
+ member_id: member,
92
+ parent_id: null,
93
+ html: comment,
94
+ status: 'published'
95
+ }, options);
96
+
97
+ if (!options.context.internal) {
98
+ await this.sendNewCommentNotifications(model);
99
+ }
100
+
101
+ DomainEvents.dispatch(MemberCommentEvent.create({
102
+ memberId: member,
103
+ postId: post,
104
+ commentId: model.id
105
+ }));
106
+
107
+ return model;
108
+ }
109
+
110
+ /**
111
+ * @param {string} parent - The ID of the Comment to reply to
112
+ * @param {string} member - The ID of the Member to comment as
113
+ * @param {string} comment - The HTML content of the Comment
114
+ * @param {any} options
115
+ */
116
+ async replyToComment(parent, member, comment, options) {
117
+ await this.models.Member.findOne({
118
+ id: member
119
+ }, {
120
+ require: true,
121
+ ...options
122
+ });
123
+
124
+ const parentComment = await this.getCommentByID(parent, options);
125
+ if (!parentComment) {
126
+ throw new errors.BadRequestError({
127
+ message: tpl(messages.commentNotFound)
128
+ });
129
+ }
130
+
131
+ if (parentComment.get('parent_id') !== null) {
132
+ throw new errors.BadRequestError({
133
+ message: tpl(messages.replyToReply)
134
+ });
135
+ }
136
+
137
+ const model = await this.models.Comment.add({
138
+ post_id: parentComment.get('post_id'),
139
+ member_id: member,
140
+ parent_id: parentComment.id,
141
+ html: comment,
142
+ status: 'published'
143
+ }, options);
144
+
145
+ if (!options.context.internal) {
146
+ await this.sendNewCommentNotifications(model);
20
147
  }
148
+
149
+ DomainEvents.dispatch(MemberCommentEvent.create({
150
+ memberId: member,
151
+ postId: parentComment.get('post_id'),
152
+ commentId: model.id
153
+ }));
154
+
155
+ return model;
156
+ }
157
+
158
+ /**
159
+ * @param {string} id - The ID of the Comment to delete
160
+ * @param {string} member - The ID of the Member to delete as
161
+ * @param {any} options
162
+ */
163
+ async deleteComment(id, member, options) {
164
+ const existingComment = await this.getCommentByID(id, options);
165
+
166
+ if (existingComment.get('member_id') !== member) {
167
+ throw new errors.NoPermissionError({
168
+ // todo fix message
169
+ message: tpl(messages.memberNotFound)
170
+ });
171
+ }
172
+
173
+ const model = await this.models.Comment.edit({
174
+ status: 'deleted'
175
+ }, {
176
+ id,
177
+ require: true,
178
+ ...options
179
+ });
180
+
181
+ return model;
182
+ }
183
+
184
+ /**
185
+ * @param {string} id - The ID of the Comment to edit
186
+ * @param {string} member - The ID of the Member to edit as
187
+ * @param {string} comment - The new HTML content of the Comment
188
+ * @param {any} options
189
+ */
190
+ async editCommentContent(id, member, comment, options) {
191
+ const existingComment = await this.getCommentByID(id, options);
192
+
193
+ if (!comment) {
194
+ return existingComment;
195
+ }
196
+
197
+ if (existingComment.get('member_id') !== member) {
198
+ throw new errors.NoPermissionError({
199
+ // todo fix message
200
+ message: tpl(messages.memberNotFound)
201
+ });
202
+ }
203
+
204
+ const model = await this.models.Comment.edit({
205
+ html: comment
206
+ }, {
207
+ id,
208
+ require: true,
209
+ ...options
210
+ });
211
+
212
+ return model;
21
213
  }
22
214
  }
23
215
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  const JobManager = require('@tryghost/job-manager');
7
7
  const logging = require('@tryghost/logging');
8
+ const models = require('../../models');
8
9
  const sentry = require('../../../shared/sentry');
9
10
 
10
11
  const errorHandler = (error, workerMeta) => {
@@ -17,6 +18,28 @@ const workerMessageHandler = ({name, message}) => {
17
18
  logging.info(`Worker for job ${name} sent a message: ${message}`);
18
19
  };
19
20
 
20
- const jobManager = new JobManager({errorHandler, workerMessageHandler});
21
+ const initTestMode = () => {
22
+ // Output job queue length every 5 seconds
23
+ setInterval(() => {
24
+ logging.warn(`${jobManager.queue.length()} jobs in the queue. Idle: ${jobManager.queue.idle()}`);
25
+
26
+ const runningScheduledjobs = Object.keys(jobManager.bree.workers);
27
+ if (Object.keys(jobManager.bree.workers).length) {
28
+ logging.warn(`${Object.keys(jobManager.bree.workers).length} jobs running: ${runningScheduledjobs}`);
29
+ }
30
+
31
+ const scheduledJobs = Object.keys(jobManager.bree.intervals);
32
+ if (Object.keys(jobManager.bree.intervals).length) {
33
+ logging.warn(`${Object.keys(jobManager.bree.intervals).length} scheduled jobs: ${scheduledJobs}`);
34
+ }
35
+
36
+ if (runningScheduledjobs.length === 0 && scheduledJobs.length === 0) {
37
+ logging.warn('No scheduled or running jobs');
38
+ }
39
+ }, 5000);
40
+ };
41
+
42
+ const jobManager = new JobManager({errorHandler, workerMessageHandler, JobModel: models.Job});
21
43
 
22
44
  module.exports = jobManager;
45
+ module.exports.initTestMode = initTestMode;
@@ -95,6 +95,7 @@ module.exports = class GhostMailer {
95
95
  * @param {string} message.html - email content
96
96
  * @param {string} message.to - email recipient address
97
97
  * @param {string} [message.from] - sender email address
98
+ * @param {string} [message.text] - text version of this message
98
99
  * @param {boolean} [message.forceTextContent] - maps to generateTextFromHTML nodemailer option
99
100
  * which is: "if set to true uses HTML to generate plain text body part from the HTML if the text is not defined"
100
101
  * (ref: https://github.com/nodemailer/nodemailer/tree/da2f1d278f91b4262e940c0b37638e7027184b1d#e-mail-message-fields)
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- const {UnauthorizedError} = require('@tryghost/errors');
2
+ const {ValidationError} = require('@tryghost/errors');
3
3
 
4
4
  class SingleUseTokenProvider {
5
5
  /**
@@ -41,7 +41,7 @@ class SingleUseTokenProvider {
41
41
  const model = await this.model.findOne({token});
42
42
 
43
43
  if (!model) {
44
- throw new UnauthorizedError({
44
+ throw new ValidationError({
45
45
  message: 'Invalid token provided'
46
46
  });
47
47
  }
@@ -51,7 +51,7 @@ class SingleUseTokenProvider {
51
51
  const tokenLifetimeMilliseconds = Date.now() - createdAtEpoch;
52
52
 
53
53
  if (tokenLifetimeMilliseconds > this.validity) {
54
- throw new UnauthorizedError({
54
+ throw new ValidationError({
55
55
  message: 'Token expired'
56
56
  });
57
57
  }
@@ -189,7 +189,8 @@ function createApiInstance(config) {
189
189
  StripeProduct: models.StripeProduct,
190
190
  StripePrice: models.StripePrice,
191
191
  Product: models.Product,
192
- Settings: models.Settings
192
+ Settings: models.Settings,
193
+ Comment: models.Comment
193
194
  },
194
195
  stripeAPIService: stripeService.api,
195
196
  offersAPI: offersService.api,
@@ -154,11 +154,14 @@ class MembersConfigProvider {
154
154
  };
155
155
  }
156
156
 
157
- getSigninURL(token, type) {
157
+ getSigninURL(token, type, referrer) {
158
158
  const siteUrl = this._urlUtils.urlFor({relativeUrl: '/members/'}, true);
159
159
  const signinURL = new URL(siteUrl);
160
160
  signinURL.searchParams.set('token', token);
161
161
  signinURL.searchParams.set('action', type);
162
+ if (referrer) {
163
+ signinURL.searchParams.set('r', referrer);
164
+ }
162
165
  return signinURL.toString();
163
166
  }
164
167
  }
@@ -143,8 +143,8 @@ const createSessionFromMagicLink = async function (req, res, next) {
143
143
  // req.query is a plain object, copy it to a URLSearchParams object so we can call toString()
144
144
  const searchParams = new URLSearchParams('');
145
145
  Object.keys(req.query).forEach((param) => {
146
- // don't copy the token param
147
- if (param !== 'token') {
146
+ // don't copy the "token" or "r" params
147
+ if (param !== 'token' && param !== 'r') {
148
148
  searchParams.set(param, req.query[param]);
149
149
  }
150
150
  });
@@ -182,6 +182,18 @@ const createSessionFromMagicLink = async function (req, res, next) {
182
182
  }
183
183
  }
184
184
 
185
+ if (action === 'signin') {
186
+ const referrer = req.query.r;
187
+ const siteUrl = urlUtils.getSiteUrl();
188
+
189
+ if (referrer && referrer.startsWith(siteUrl)) {
190
+ const redirectUrl = new URL(referrer);
191
+ redirectUrl.searchParams.set('success', true);
192
+ redirectUrl.searchParams.set('action', 'signin');
193
+ return res.redirect(redirectUrl.pathname + redirectUrl.search);
194
+ }
195
+ }
196
+
185
197
  // Do a standard 302 redirect to the homepage, with success=true
186
198
  searchParams.set('success', true);
187
199
  res.redirect(`${urlUtils.getSubdir()}/?${searchParams.toString()}`);