ghost 5.4.1 → 5.7.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 (117) hide show
  1. package/PRIVACY.md +3 -2
  2. package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-0.0.0.tgz +0 -0
  4. package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
  5. package/components/tryghost-constants-0.0.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  7. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  10. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
  13. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  14. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  15. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  16. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  17. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  18. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  19. package/components/tryghost-members-csv-0.0.0.tgz +0 -0
  20. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  21. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  22. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  23. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  24. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  25. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  26. package/components/tryghost-minifier-0.0.0.tgz +0 -0
  27. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  28. package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
  29. package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
  30. package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
  31. package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
  32. package/components/tryghost-package-json-0.0.0.tgz +0 -0
  33. package/components/tryghost-security-0.0.0.tgz +0 -0
  34. package/components/tryghost-session-service-0.0.0.tgz +0 -0
  35. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  36. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  37. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  38. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  39. package/content/themes/casper/assets/built/global.css +1 -1
  40. package/content/themes/casper/assets/built/global.css.map +1 -1
  41. package/content/themes/casper/assets/built/screen.css +1 -1
  42. package/content/themes/casper/assets/built/screen.css.map +1 -1
  43. package/content/themes/casper/assets/css/screen.css +9 -1
  44. package/content/themes/casper/gulpfile.js +1 -1
  45. package/content/themes/casper/package.json +9 -9
  46. package/content/themes/casper/yarn.lock +1154 -1249
  47. package/core/boot.js +6 -1
  48. package/core/built/assets/{chunk.3.dc389a0f93cb5fabd695.js → chunk.3.33097bb5eb150719bdd2.js} +19 -19
  49. package/core/built/assets/fonts/Inter.ttf +0 -0
  50. package/core/built/assets/ghost-dark-1bdd57aba1fa4a23388121740454dab2.css +1 -0
  51. package/core/built/assets/ghost.min-8f5c061e0892b93adecc2b9e37ad2f3a.css +1 -0
  52. package/core/built/assets/{ghost.min-36b64813b14c45075770658269d4b478.js → ghost.min-ff9ba089fd81cb40831f4b62e63a2ca9.js} +3015 -2874
  53. package/core/built/assets/icons/event-comment.svg +3 -0
  54. package/core/built/assets/{vendor.min-be0129c9c6897c9f10425e2402881d77.js → vendor.min-3dd40d3052381526f38fd290d13baa47.js} +2394 -924
  55. package/core/frontend/helpers/comments.js +39 -14
  56. package/core/frontend/helpers/ghost_head.js +22 -4
  57. package/core/frontend/helpers/img_url.js +67 -6
  58. package/core/frontend/utils/frontend-apps.js +33 -0
  59. package/core/frontend/web/middleware/handle-image-sizes.js +7 -11
  60. package/core/server/api/endpoints/{comments-comments.js → comments-members.js} +24 -43
  61. package/core/server/api/endpoints/index.js +2 -6
  62. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  63. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
  64. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
  65. package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +1 -0
  66. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +11 -0
  67. package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
  68. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +4 -0
  69. package/core/server/data/exporter/table-lists.js +2 -1
  70. package/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js +0 -1
  71. package/core/server/data/migrations/versions/5.3/2022-07-06-09-26-add-ghost-explore-integration-api-key.js +0 -1
  72. package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
  73. package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
  74. package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
  75. package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
  76. package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
  77. package/core/server/data/migrations/versions/5.6/2022-07-27-13-40-change-explore-type.js +24 -0
  78. package/core/server/data/schema/commands.js +7 -2
  79. package/core/server/data/schema/fixtures/fixtures.json +6 -1
  80. package/core/server/data/schema/schema.js +12 -4
  81. package/core/server/ghost-server.js +0 -22
  82. package/core/server/models/comment-report.js +34 -0
  83. package/core/server/models/comment.js +8 -7
  84. package/core/server/models/job.js +9 -0
  85. package/core/server/services/bulk-email/bulk-email-processor.js +6 -0
  86. package/core/server/services/comments/controller.js +82 -0
  87. package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
  88. package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
  89. package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
  90. package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
  91. package/core/server/services/comments/email-templates/report.hbs +199 -0
  92. package/core/server/services/comments/email-templates/report.txt.js +16 -0
  93. package/core/server/services/comments/emails.js +57 -1
  94. package/core/server/services/comments/index.js +6 -1
  95. package/core/server/services/comments/service.js +291 -9
  96. package/core/server/services/jobs/job-service.js +24 -1
  97. package/core/server/services/mail/GhostMailer.js +1 -0
  98. package/core/server/services/mega/email-preview.js +5 -1
  99. package/core/server/services/mega/mega.js +2 -4
  100. package/core/server/services/mega/post-email-serializer.js +97 -2
  101. package/core/server/services/mega/segment-parser.js +10 -1
  102. package/core/server/services/members/api.js +2 -1
  103. package/core/server/services/members/service.js +9 -4
  104. package/core/server/services/public-config/config.js +2 -1
  105. package/core/server/services/stripe/service.js +9 -1
  106. package/core/server/web/admin/views/default-prod.html +4 -4
  107. package/core/server/web/admin/views/default.html +4 -4
  108. package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
  109. package/core/server/web/api/testmode/routes.js +14 -0
  110. package/core/server/web/comments/routes.js +10 -8
  111. package/core/shared/config/defaults.json +12 -7
  112. package/core/shared/config/env/config.testing.json +3 -2
  113. package/core/shared/labs.js +5 -2
  114. package/package.json +92 -59
  115. package/yarn.lock +1821 -2011
  116. package/core/built/assets/ghost-dark-739c1f5546bd048eeeb253965ef36712.css +0 -1
  117. package/core/built/assets/ghost.min-5211776b9497f36fac8c9e5f2584cbcc.css +0 -1
@@ -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,6 +1,7 @@
1
1
  class CommentsServiceWrapper {
2
2
  init() {
3
3
  const CommentsService = require('./service');
4
+ const CommentsController = require('./controller');
4
5
 
5
6
  const config = require('../../../shared/config');
6
7
  const logging = require('@tryghost/logging');
@@ -10,6 +11,7 @@ class CommentsServiceWrapper {
10
11
  const settingsCache = require('../../../shared/settings-cache');
11
12
  const urlService = require('../url');
12
13
  const urlUtils = require('../../../shared/url-utils');
14
+ const membersService = require('../members');
13
15
 
14
16
  this.api = new CommentsService({
15
17
  config,
@@ -18,8 +20,11 @@ class CommentsServiceWrapper {
18
20
  mailer,
19
21
  settingsCache,
20
22
  urlService,
21
- urlUtils
23
+ urlUtils,
24
+ contentGating: membersService.contentGating
22
25
  });
26
+
27
+ this.controller = new CommentsController(this.api);
23
28
  }
24
29
  }
25
30
 
@@ -1,23 +1,305 @@
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
+ commentsNotEnabled: 'Comments are not enabled for this site.',
13
+ cannotCommentOnPost: 'You do not have permission to comment on this post.'
14
+ };
15
+
1
16
  class CommentsService {
2
- constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils}) {
3
- this.config = config;
4
- this.logging = logging;
17
+ constructor({config, logging, models, mailer, settingsCache, urlService, urlUtils, contentGating}) {
18
+ /** @private */
5
19
  this.models = models;
6
- this.mailer = mailer;
20
+
21
+ /** @private */
7
22
  this.settingsCache = settingsCache;
8
- this.urlService = urlService;
9
- this.urlUtils = urlUtils;
23
+
24
+ /** @private */
25
+ this.contentGating = contentGating;
10
26
 
11
27
  const Emails = require('./emails');
12
- this.emails = new Emails(this);
28
+ /** @private */
29
+ this.emails = new Emails({
30
+ config,
31
+ logging,
32
+ models,
33
+ mailer,
34
+ settingsCache,
35
+ urlService,
36
+ urlUtils
37
+ });
13
38
  }
14
39
 
40
+ /**
41
+ * @returns {'off'|'all'|'paid'}
42
+ */
43
+ get enabled() {
44
+ const setting = this.settingsCache.get('comments_enabled');
45
+ if (setting === 'off' || setting === 'all' || setting === 'paid') {
46
+ return setting;
47
+ }
48
+ return 'off';
49
+ }
50
+
51
+ /** @private */
52
+ checkEnabled() {
53
+ if (this.enabled === 'off') {
54
+ throw new errors.MethodNotAllowedError({
55
+ message: tpl(messages.commentsNotEnabled)
56
+ });
57
+ }
58
+ }
59
+
60
+ /** @private */
61
+ checkCommentAccess(memberModel) {
62
+ if (this.enabled === 'paid' && memberModel.get('status') === 'free') {
63
+ throw new errors.NoPermissionError({
64
+ message: tpl(messages.cannotCommentOnPost)
65
+ });
66
+ }
67
+ }
68
+
69
+ /** @private */
70
+ checkPostAccess(postModel, memberModel) {
71
+ const access = this.contentGating.checkPostAccess(postModel.toJSON(), memberModel.toJSON());
72
+ if (access === this.contentGating.BLOCK_ACCESS) {
73
+ throw new errors.NoPermissionError({
74
+ message: tpl(messages.cannotCommentOnPost)
75
+ });
76
+ }
77
+ }
78
+
79
+ /** @private */
15
80
  async sendNewCommentNotifications(comment) {
16
- this.emails.notifyPostAuthors(comment);
81
+ await this.emails.notifyPostAuthors(comment);
17
82
 
18
83
  if (comment.get('parent_id')) {
19
- this.emails.notifyParentCommentAuthor(comment);
84
+ await this.emails.notifyParentCommentAuthor(comment);
85
+ }
86
+ }
87
+
88
+ async reportComment(commentId, reporter) {
89
+ this.checkEnabled();
90
+ const comment = await this.models.Comment.findOne({id: commentId}, {require: true});
91
+
92
+ // Check if this reporter already reported this comment (then don't send an email)?
93
+ const existing = await this.models.CommentReport.findOne({
94
+ comment_id: comment.id,
95
+ member_id: reporter.id
96
+ });
97
+
98
+ if (existing) {
99
+ // Ignore silently for now
100
+ return;
101
+ }
102
+
103
+ // Save report model
104
+ await this.models.CommentReport.add({
105
+ comment_id: comment.id,
106
+ member_id: reporter.id
107
+ });
108
+
109
+ await this.emails.notifiyReport(comment, reporter);
110
+ }
111
+
112
+ /**
113
+ * @param {any} options
114
+ */
115
+ async getComments(options) {
116
+ this.checkEnabled();
117
+ const page = await this.models.Comment.findPage(options);
118
+
119
+ return page;
120
+ }
121
+
122
+ /**
123
+ * @param {string} id - The ID of the Comment to get
124
+ * @param {any} options
125
+ */
126
+ async getCommentByID(id, options) {
127
+ this.checkEnabled();
128
+ const model = await this.models.Comment.findOne({id}, options);
129
+
130
+ if (!model) {
131
+ throw new errors.NotFoundError({
132
+ messages: tpl(messages.commentNotFound)
133
+ });
134
+ }
135
+
136
+ return model;
137
+ }
138
+
139
+ /**
140
+ * @param {string} post - The ID of the Post to comment on
141
+ * @param {string} member - The ID of the Member to comment as
142
+ * @param {string} comment - The HTML content of the Comment
143
+ * @param {any} options
144
+ */
145
+ async commentOnPost(post, member, comment, options) {
146
+ this.checkEnabled();
147
+ const memberModel = await this.models.Member.findOne({
148
+ id: member
149
+ }, {
150
+ require: true,
151
+ ...options
152
+ });
153
+
154
+ this.checkCommentAccess(memberModel);
155
+
156
+ const postModel = await this.models.Post.findOne({
157
+ id: post
158
+ }, {
159
+ require: true,
160
+ ...options
161
+ });
162
+
163
+ this.checkPostAccess(postModel, memberModel);
164
+
165
+ const model = await this.models.Comment.add({
166
+ post_id: post,
167
+ member_id: member,
168
+ parent_id: null,
169
+ html: comment,
170
+ status: 'published'
171
+ }, options);
172
+
173
+ if (!options.context.internal) {
174
+ await this.sendNewCommentNotifications(model);
175
+ }
176
+
177
+ DomainEvents.dispatch(MemberCommentEvent.create({
178
+ memberId: member,
179
+ postId: post,
180
+ commentId: model.id
181
+ }));
182
+
183
+ return model;
184
+ }
185
+
186
+ /**
187
+ * @param {string} parent - The ID of the Comment to reply to
188
+ * @param {string} member - The ID of the Member to comment as
189
+ * @param {string} comment - The HTML content of the Comment
190
+ * @param {any} options
191
+ */
192
+ async replyToComment(parent, member, comment, options) {
193
+ this.checkEnabled();
194
+ const memberModel = await this.models.Member.findOne({
195
+ id: member
196
+ }, {
197
+ require: true,
198
+ ...options
199
+ });
200
+
201
+ this.checkCommentAccess(memberModel);
202
+
203
+ const parentComment = await this.getCommentByID(parent, options);
204
+ if (!parentComment) {
205
+ throw new errors.BadRequestError({
206
+ message: tpl(messages.commentNotFound)
207
+ });
208
+ }
209
+
210
+ if (parentComment.get('parent_id') !== null) {
211
+ throw new errors.BadRequestError({
212
+ message: tpl(messages.replyToReply)
213
+ });
214
+ }
215
+ const postModel = await this.models.Post.findOne({
216
+ id: parentComment.get('post_id')
217
+ }, {
218
+ require: true,
219
+ ...options
220
+ });
221
+
222
+ this.checkPostAccess(postModel, memberModel);
223
+
224
+ const model = await this.models.Comment.add({
225
+ post_id: parentComment.get('post_id'),
226
+ member_id: member,
227
+ parent_id: parentComment.id,
228
+ html: comment,
229
+ status: 'published'
230
+ }, options);
231
+
232
+ if (!options.context.internal) {
233
+ await this.sendNewCommentNotifications(model);
20
234
  }
235
+
236
+ DomainEvents.dispatch(MemberCommentEvent.create({
237
+ memberId: member,
238
+ postId: parentComment.get('post_id'),
239
+ commentId: model.id
240
+ }));
241
+
242
+ return model;
243
+ }
244
+
245
+ /**
246
+ * @param {string} id - The ID of the Comment to delete
247
+ * @param {string} member - The ID of the Member to delete as
248
+ * @param {any} options
249
+ */
250
+ async deleteComment(id, member, options) {
251
+ this.checkEnabled();
252
+ const existingComment = await this.getCommentByID(id, options);
253
+
254
+ if (existingComment.get('member_id') !== member) {
255
+ throw new errors.NoPermissionError({
256
+ // todo fix message
257
+ message: tpl(messages.memberNotFound)
258
+ });
259
+ }
260
+
261
+ const model = await this.models.Comment.edit({
262
+ status: 'deleted'
263
+ }, {
264
+ id,
265
+ require: true,
266
+ ...options
267
+ });
268
+
269
+ return model;
270
+ }
271
+
272
+ /**
273
+ * @param {string} id - The ID of the Comment to edit
274
+ * @param {string} member - The ID of the Member to edit as
275
+ * @param {string} comment - The new HTML content of the Comment
276
+ * @param {any} options
277
+ */
278
+ async editCommentContent(id, member, comment, options) {
279
+ this.checkEnabled();
280
+ const existingComment = await this.getCommentByID(id, options);
281
+
282
+ if (!comment) {
283
+ return existingComment;
284
+ }
285
+
286
+ if (existingComment.get('member_id') !== member) {
287
+ throw new errors.NoPermissionError({
288
+ // todo fix message
289
+ message: tpl(messages.memberNotFound)
290
+ });
291
+ }
292
+
293
+ const model = await this.models.Comment.edit({
294
+ html: comment,
295
+ edited_at: new Date()
296
+ }, {
297
+ id,
298
+ require: true,
299
+ ...options
300
+ });
301
+
302
+ return model;
21
303
  }
22
304
  }
23
305