ghost 5.2.2 → 5.3.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 (259) hide show
  1. package/content/themes/casper/assets/built/screen.css +1 -1
  2. package/content/themes/casper/assets/built/screen.css.map +1 -1
  3. package/content/themes/casper/assets/css/screen.css +31 -1
  4. package/content/themes/casper/default.hbs +1 -0
  5. package/content/themes/casper/package.json +1 -1
  6. package/content/themes/casper/partials/icons/search.hbs +1 -0
  7. package/core/boot.js +3 -1
  8. package/core/bridge.js +7 -0
  9. package/core/built/assets/codemirror/{codemirror-d25c379b87ec8b33d54ac7149bc0b6ae.js → codemirror-30201ab2cff61db847193fc9f5ed922d.js} +1 -1
  10. package/core/built/assets/ghost-dark-9e5d1f0dfae41232e5e34e4d0df53ae0.css +1 -0
  11. package/core/built/assets/ghost.min-e7cfbd1800f8e99b9158f74f1e39cd76.css +1 -0
  12. package/core/built/assets/{ghost.min-971a0ff706bbf9e24f0b0a5770c8fc41.js → ghost.min-f4bba3a2a5ef256b82641345505d4f0f.js} +69 -66
  13. package/core/built/assets/icons/activity-placeholder.svg +3 -0
  14. package/core/built/assets/simplemde/{simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js → simplemde-fb4527da6e489b34dc24b28ea870b286.js} +1 -1
  15. package/core/built/assets/{vendor.min-ea369e6487643585f35409d474b06789.js → vendor.min-4076498ccd6c8412365f43b156084ed8.js} +119 -117
  16. package/core/built/assets/{vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css → vendor.min-4a6661c574707ceca220aa2e76558995.css} +2 -1
  17. package/core/frontend/helpers/comment_count.js +25 -0
  18. package/core/frontend/helpers/comments.js +67 -0
  19. package/core/frontend/helpers/date.js +3 -1
  20. package/core/frontend/helpers/ghost_head.js +13 -0
  21. package/core/frontend/services/admin-auth-assets/index.js +4 -0
  22. package/core/frontend/services/admin-auth-assets/service.js +93 -0
  23. package/core/frontend/services/comment-counts-assets/index.js +4 -0
  24. package/core/frontend/services/comment-counts-assets/service.js +59 -0
  25. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +5 -1
  26. package/core/frontend/src/admin-auth/index.html +5 -0
  27. package/core/frontend/src/admin-auth/message-handler.js +75 -0
  28. package/core/frontend/src/comment-counts/js/comment-counts.js +71 -0
  29. package/core/frontend/web/site.js +3 -0
  30. package/core/server/api/{canary → endpoints}/actions.js +0 -0
  31. package/core/server/api/{canary → endpoints}/authentication.js +0 -0
  32. package/core/server/api/{canary → endpoints}/authors-public.js +0 -0
  33. package/core/server/api/endpoints/comments-comments.js +247 -0
  34. package/core/server/api/endpoints/comments.js +25 -0
  35. package/core/server/api/{canary → endpoints}/config.js +0 -0
  36. package/core/server/api/{canary → endpoints}/custom-theme-settings.js +0 -0
  37. package/core/server/api/{canary → endpoints}/db.js +0 -0
  38. package/core/server/api/{canary → endpoints}/email-post.js +0 -0
  39. package/core/server/api/{canary → endpoints}/email-previews.js +0 -0
  40. package/core/server/api/{canary → endpoints}/emails.js +0 -0
  41. package/core/server/api/endpoints/explore.js +12 -0
  42. package/core/server/api/{canary → endpoints}/files.js +0 -0
  43. package/core/server/api/{canary → endpoints}/identities.js +0 -0
  44. package/core/server/api/{canary → endpoints}/images.js +0 -0
  45. package/core/server/api/{canary → endpoints}/index.js +16 -0
  46. package/core/server/api/{canary → endpoints}/integrations.js +0 -0
  47. package/core/server/api/{canary → endpoints}/invites.js +0 -0
  48. package/core/server/api/{canary → endpoints}/labels.js +0 -0
  49. package/core/server/api/{canary → endpoints}/mail.js +0 -0
  50. package/core/server/api/{canary → endpoints}/media.js +0 -0
  51. package/core/server/api/{canary → endpoints}/member-signin-urls.js +0 -0
  52. package/core/server/api/{canary → endpoints}/members-stripe-connect.js +0 -0
  53. package/core/server/api/{canary → endpoints}/members.js +0 -0
  54. package/core/server/api/{canary → endpoints}/newsletters-public.js +0 -0
  55. package/core/server/api/{canary → endpoints}/newsletters.js +0 -0
  56. package/core/server/api/{canary → endpoints}/notifications.js +0 -0
  57. package/core/server/api/{canary → endpoints}/oembed.js +0 -0
  58. package/core/server/api/{canary → endpoints}/offers-public.js +0 -0
  59. package/core/server/api/{canary → endpoints}/offers.js +0 -0
  60. package/core/server/api/{canary → endpoints}/pages-public.js +0 -0
  61. package/core/server/api/{canary → endpoints}/pages.js +0 -0
  62. package/core/server/api/{canary → endpoints}/posts-public.js +0 -0
  63. package/core/server/api/{canary → endpoints}/posts.js +0 -0
  64. package/core/server/api/{canary → endpoints}/previews.js +0 -0
  65. package/core/server/api/{canary → endpoints}/redirects.js +0 -0
  66. package/core/server/api/{canary → endpoints}/roles.js +0 -0
  67. package/core/server/api/{canary → endpoints}/schedules.js +0 -0
  68. package/core/server/api/{canary → endpoints}/session.js +0 -0
  69. package/core/server/api/{canary → endpoints}/settings-public.js +0 -0
  70. package/core/server/api/{canary → endpoints}/settings.js +0 -0
  71. package/core/server/api/{canary → endpoints}/site.js +0 -0
  72. package/core/server/api/{canary → endpoints}/slack.js +0 -0
  73. package/core/server/api/{canary → endpoints}/slugs.js +0 -0
  74. package/core/server/api/{canary → endpoints}/snippets.js +0 -0
  75. package/core/server/api/{canary → endpoints}/stats.js +0 -0
  76. package/core/server/api/{canary → endpoints}/tags-public.js +0 -0
  77. package/core/server/api/{canary → endpoints}/tags.js +0 -0
  78. package/core/server/api/{canary → endpoints}/themes.js +0 -0
  79. package/core/server/api/{canary → endpoints}/tiers-public.js +0 -0
  80. package/core/server/api/{canary → endpoints}/tiers.js +0 -0
  81. package/core/server/api/{canary → endpoints}/users.js +0 -0
  82. package/core/server/api/{canary → endpoints}/utils/index.js +0 -0
  83. package/core/server/api/{canary → endpoints}/utils/permissions.js +3 -3
  84. package/core/server/api/{canary → endpoints}/utils/serializers/index.js +0 -0
  85. package/core/server/api/{canary → endpoints}/utils/serializers/input/authors.js +1 -1
  86. package/core/server/api/{canary → endpoints}/utils/serializers/input/db.js +1 -1
  87. package/core/server/api/{canary → endpoints}/utils/serializers/input/index.js +0 -0
  88. package/core/server/api/{canary → endpoints}/utils/serializers/input/integrations.js +1 -1
  89. package/core/server/api/{canary → endpoints}/utils/serializers/input/media.js +0 -0
  90. package/core/server/api/{canary → endpoints}/utils/serializers/input/members.js +1 -1
  91. package/core/server/api/{canary → endpoints}/utils/serializers/input/pages.js +1 -1
  92. package/core/server/api/{canary → endpoints}/utils/serializers/input/posts.js +1 -1
  93. package/core/server/api/{canary → endpoints}/utils/serializers/input/settings.js +2 -1
  94. package/core/server/api/{canary → endpoints}/utils/serializers/input/tags.js +1 -1
  95. package/core/server/api/{canary → endpoints}/utils/serializers/input/tiers.js +0 -0
  96. package/core/server/api/{canary → endpoints}/utils/serializers/input/users.js +1 -1
  97. package/core/server/api/{canary → endpoints}/utils/serializers/input/utils/slug-filter-order.js +0 -0
  98. package/core/server/api/{canary → endpoints}/utils/serializers/input/utils/url.js +0 -0
  99. package/core/server/api/{canary → endpoints}/utils/serializers/input/webhooks.js +1 -1
  100. package/core/server/api/{canary → endpoints}/utils/serializers/output/all.js +1 -1
  101. package/core/server/api/{canary → endpoints}/utils/serializers/output/authentication.js +1 -1
  102. package/core/server/api/endpoints/utils/serializers/output/comments.js +5 -0
  103. package/core/server/api/{canary → endpoints}/utils/serializers/output/config.js +1 -1
  104. package/core/server/api/{canary → endpoints}/utils/serializers/output/custom-theme-settings.js +0 -0
  105. package/core/server/api/{canary → endpoints}/utils/serializers/output/db.js +1 -1
  106. package/core/server/api/{canary → endpoints}/utils/serializers/output/default.js +1 -1
  107. package/core/server/api/{canary → endpoints}/utils/serializers/output/email-posts.js +0 -0
  108. package/core/server/api/endpoints/utils/serializers/output/explore.js +11 -0
  109. package/core/server/api/{canary → endpoints}/utils/serializers/output/files.js +0 -0
  110. package/core/server/api/{canary → endpoints}/utils/serializers/output/images.js +1 -1
  111. package/core/server/api/{canary → endpoints}/utils/serializers/output/index.js +8 -0
  112. package/core/server/api/{canary → endpoints}/utils/serializers/output/mail.js +1 -1
  113. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/actions.js +0 -0
  114. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/authors.js +0 -0
  115. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +50 -0
  116. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/emails.js +0 -0
  117. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/images.js +0 -0
  118. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/index.js +1 -0
  119. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/integrations.js +0 -0
  120. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/newsletters.js +0 -0
  121. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/pages.js +0 -0
  122. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/posts.js +0 -0
  123. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/settings.js +0 -0
  124. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/snippets.js +0 -0
  125. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/tags.js +0 -0
  126. package/core/server/api/{canary → endpoints}/utils/serializers/output/mappers/users.js +0 -0
  127. package/core/server/api/{canary → endpoints}/utils/serializers/output/media.js +0 -0
  128. package/core/server/api/{canary → endpoints}/utils/serializers/output/members-stripe-connect.js +0 -0
  129. package/core/server/api/{canary → endpoints}/utils/serializers/output/members.js +1 -1
  130. package/core/server/api/{canary → endpoints}/utils/serializers/output/notifications.js +1 -1
  131. package/core/server/api/{canary → endpoints}/utils/serializers/output/oembed.js +1 -1
  132. package/core/server/api/{canary → endpoints}/utils/serializers/output/offers.js +1 -1
  133. package/core/server/api/{canary → endpoints}/utils/serializers/output/pages.js +1 -1
  134. package/core/server/api/{canary → endpoints}/utils/serializers/output/posts.js +1 -1
  135. package/core/server/api/{canary → endpoints}/utils/serializers/output/previews.js +0 -0
  136. package/core/server/api/{canary → endpoints}/utils/serializers/output/redirects.js +0 -0
  137. package/core/server/api/{canary → endpoints}/utils/serializers/output/roles.js +1 -1
  138. package/core/server/api/{canary → endpoints}/utils/serializers/output/schedules.js +0 -0
  139. package/core/server/api/{canary → endpoints}/utils/serializers/output/session.js +1 -1
  140. package/core/server/api/{canary → endpoints}/utils/serializers/output/settings.js +0 -0
  141. package/core/server/api/{canary → endpoints}/utils/serializers/output/site.js +1 -1
  142. package/core/server/api/{canary → endpoints}/utils/serializers/output/slack.js +1 -1
  143. package/core/server/api/{canary → endpoints}/utils/serializers/output/slugs.js +1 -1
  144. package/core/server/api/{canary → endpoints}/utils/serializers/output/themes.js +1 -1
  145. package/core/server/api/{canary → endpoints}/utils/serializers/output/tiers.js +1 -1
  146. package/core/server/api/{canary → endpoints}/utils/serializers/output/users.js +1 -1
  147. package/core/server/api/{canary → endpoints}/utils/serializers/output/utils/clean.js +2 -1
  148. package/core/server/api/{canary → endpoints}/utils/serializers/output/utils/date.js +0 -0
  149. package/core/server/api/endpoints/utils/serializers/output/utils/extra-attrs.js +67 -0
  150. package/core/server/api/{canary → endpoints}/utils/serializers/output/utils/post-gating.js +0 -0
  151. package/core/server/api/{canary → endpoints}/utils/serializers/output/utils/url.js +1 -1
  152. package/core/server/api/{canary → endpoints}/utils/validators/index.js +0 -0
  153. package/core/server/api/{canary → endpoints}/utils/validators/input/files.js +0 -0
  154. package/core/server/api/{canary → endpoints}/utils/validators/input/images.js +0 -0
  155. package/core/server/api/{canary → endpoints}/utils/validators/input/index.js +0 -0
  156. package/core/server/api/{canary → endpoints}/utils/validators/input/invitations.js +1 -1
  157. package/core/server/api/{canary → endpoints}/utils/validators/input/invites.js +0 -0
  158. package/core/server/api/{canary → endpoints}/utils/validators/input/labels.js +0 -0
  159. package/core/server/api/{canary → endpoints}/utils/validators/input/media.js +0 -0
  160. package/core/server/api/{canary → endpoints}/utils/validators/input/members.js +0 -0
  161. package/core/server/api/{canary → endpoints}/utils/validators/input/oembed.js +0 -0
  162. package/core/server/api/{canary → endpoints}/utils/validators/input/pages.js +0 -0
  163. package/core/server/api/{canary → endpoints}/utils/validators/input/password_reset.js +1 -1
  164. package/core/server/api/{canary → endpoints}/utils/validators/input/posts.js +0 -0
  165. package/core/server/api/{canary → endpoints}/utils/validators/input/settings.js +0 -0
  166. package/core/server/api/{canary → endpoints}/utils/validators/input/setup.js +1 -1
  167. package/core/server/api/{canary → endpoints}/utils/validators/input/snippets.js +0 -0
  168. package/core/server/api/{canary → endpoints}/utils/validators/input/tags.js +0 -0
  169. package/core/server/api/{canary → endpoints}/utils/validators/input/tiers.js +0 -0
  170. package/core/server/api/{canary → endpoints}/utils/validators/input/users.js +1 -1
  171. package/core/server/api/{canary → endpoints}/utils/validators/input/webhooks.js +0 -0
  172. package/core/server/api/{canary → endpoints}/utils/validators/output/index.js +0 -0
  173. package/core/server/api/{canary → endpoints}/utils/validators/utils/json-schema.js +0 -0
  174. package/core/server/api/{canary → endpoints}/webhooks.js +0 -0
  175. package/core/server/api/index.js +1 -1
  176. package/core/server/data/exporter/table-lists.js +4 -2
  177. package/core/server/data/migrations/versions/5.3/2022-07-04-13-49-add-comments-table.js +13 -0
  178. package/core/server/data/migrations/versions/5.3/2022-07-05-09-36-add-comments-likes-table.js +9 -0
  179. package/core/server/data/migrations/versions/5.3/2022-07-05-09-47-add-comments-reports-table.js +10 -0
  180. package/core/server/data/migrations/versions/5.3/2022-07-05-10-00-add-comment-related-fields-to-members.js +21 -0
  181. package/core/server/data/migrations/versions/5.3/2022-07-05-12-55-add-comments-crud-permissions.js +68 -0
  182. package/core/server/data/migrations/versions/5.3/2022-07-05-15-35-add-comment-notifications-field-to-users-table.js +7 -0
  183. package/core/server/data/migrations/versions/5.3/2022-07-06-07-26-add-comments-enabled-setting.js +8 -0
  184. package/core/server/data/migrations/versions/5.3/2022-07-06-07-58-add-ghost-explore-integration-role.js +31 -0
  185. package/core/server/data/migrations/versions/5.3/2022-07-06-09-13-add-ghost-explore-integration-role-permissions.js +11 -0
  186. package/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js +38 -0
  187. package/core/server/data/migrations/versions/5.3/2022-07-06-09-26-add-ghost-explore-integration-api-key.js +73 -0
  188. package/core/server/data/schema/default-settings/default-settings.json +14 -0
  189. package/core/server/data/schema/fixtures/fixtures.json +65 -2
  190. package/core/server/data/schema/schema.js +31 -1
  191. package/core/server/models/base/plugins/crud.js +15 -0
  192. package/core/server/models/comment-like.js +34 -0
  193. package/core/server/models/comment.js +163 -0
  194. package/core/server/models/member.js +2 -1
  195. package/core/server/models/user.js +2 -1
  196. package/core/server/services/auth/session/express-session.js +1 -1
  197. package/core/server/services/comments/email-templates/new-comment-reply.hbs +199 -0
  198. package/core/server/services/comments/email-templates/new-comment-reply.txt.js +14 -0
  199. package/core/server/services/comments/email-templates/new-comment.hbs +199 -0
  200. package/core/server/services/comments/email-templates/new-comment.txt.js +14 -0
  201. package/core/server/services/comments/emails.js +164 -0
  202. package/core/server/services/comments/index.js +26 -0
  203. package/core/server/services/comments/service.js +24 -0
  204. package/core/server/services/explore/index.js +18 -0
  205. package/core/server/services/explore/service.js +55 -0
  206. package/core/server/services/members/middleware.js +4 -4
  207. package/core/server/services/members/utils.js +2 -1
  208. package/core/server/services/permissions/can-this.js +18 -5
  209. package/core/server/services/permissions/parse-context.js +5 -0
  210. package/core/server/services/permissions/providers.js +17 -1
  211. package/core/server/services/posts/posts-service.js +14 -0
  212. package/core/server/services/users.js +84 -1
  213. package/core/server/web/admin/app.js +4 -0
  214. package/core/server/web/admin/views/default-prod.html +5 -5
  215. package/core/server/web/admin/views/default.html +5 -5
  216. package/core/server/web/api/app.js +2 -2
  217. package/core/server/web/api/{canary → endpoints}/admin/app.js +4 -4
  218. package/core/server/web/api/{canary → endpoints}/admin/middleware.js +1 -0
  219. package/core/server/web/api/{canary → endpoints}/admin/routes.js +6 -1
  220. package/core/server/web/api/{canary → endpoints}/content/app.js +4 -4
  221. package/core/server/web/api/{canary → endpoints}/content/middleware.js +0 -0
  222. package/core/server/web/api/{canary → endpoints}/content/routes.js +1 -1
  223. package/core/server/web/api/testmode/routes.js +1 -1
  224. package/core/server/web/comments/index.js +1 -0
  225. package/core/server/web/comments/routes.js +28 -0
  226. package/core/server/web/members/app.js +5 -0
  227. package/core/server/web/parent/frontend.js +1 -0
  228. package/core/shared/config/defaults.json +16 -6
  229. package/core/shared/config/env/config.testing.json +4 -0
  230. package/core/shared/config/overrides.json +1 -0
  231. package/core/shared/labs.js +2 -1
  232. package/core/shared/settings-cache/public.js +2 -1
  233. package/package.json +20 -20
  234. package/yarn.lock +425 -275
  235. package/Gruntfile.js +0 -384
  236. package/core/built/assets/ghost-dark-923c90399aa560625a983a6ae9abb38c.css +0 -1
  237. package/core/built/assets/ghost.min-c74f50609022cebf13ad28810def68b6.css +0 -1
  238. package/core/built/assets/img/contributors/AileenCGN-bf8b9ffbb34c0fd93beb8136af07771b.jpeg +0 -0
  239. package/core/built/assets/img/contributors/ErisDS-c958ccb9e3597320dee745a42f478569.jpeg +0 -0
  240. package/core/built/assets/img/contributors/GeorginaLusby-6cea5defddee3c4ea7320e580521e832.jpeg +0 -0
  241. package/core/built/assets/img/contributors/JohnONolan-47041b80c35c6341b9b929b03139aecc.jpeg +0 -0
  242. package/core/built/assets/img/contributors/acburdine-d9777fe2601dc215afb6723315829c89.jpeg +0 -0
  243. package/core/built/assets/img/contributors/bnookala-5896fcdd2f477495323e420efe890657.jpeg +0 -0
  244. package/core/built/assets/img/contributors/cobbspur-07ded67009757d12517621fc856eba62.jpeg +0 -0
  245. package/core/built/assets/img/contributors/dbalders-452347a406c2ca23657daea9100878f3.jpeg +0 -0
  246. package/core/built/assets/img/contributors/disordinary-b9997e5debb59b7aadc79ba90955b662.jpeg +0 -0
  247. package/core/built/assets/img/contributors/felixrieseberg-ecf29e8eadc58fab999c507049f898cf.jpeg +0 -0
  248. package/core/built/assets/img/contributors/frantzypants-637b03f85dff89700a661fde79daea5c.jpeg +0 -0
  249. package/core/built/assets/img/contributors/halfdan-00d6783e5fba2900ee1380939297d8ee.jpeg +0 -0
  250. package/core/built/assets/img/contributors/jaswilli-8cc9a8d2539ca03239d113dfb25ff5c2.jpeg +0 -0
  251. package/core/built/assets/img/contributors/kevinansfield-925606c55bc2f3f2f05c0fa58b953ad1.jpeg +0 -0
  252. package/core/built/assets/img/contributors/kevinkucharczyk-3c7dfe2a103a83737b9d5ee8e19d67f8.jpeg +0 -0
  253. package/core/built/assets/img/contributors/kirrg001-79823418f2ca21e81719653f0286f95b.jpeg +0 -0
  254. package/core/built/assets/img/contributors/mixonic-1ff87736dd02cfa080ae109b45131aa6.png +0 -0
  255. package/core/built/assets/img/contributors/rwjblue-5c7cc009cda45baca2d45f0d1ed19e48.jpeg +0 -0
  256. package/core/built/assets/img/contributors/sebgie-0fb02df00ee7834dbcc8beba84aec81e.png +0 -0
  257. package/core/built/assets/img/contributors/tgriesser-d871cbf74a871c0fb6d855e76a893f7e.png +0 -0
  258. package/core/server/api/canary/utils/serializers/output/utils/extra-attrs.js +0 -33
  259. package/urls.json +0 -597
@@ -0,0 +1,11 @@
1
+ const {addPermissionWithRoles} = require('../../utils');
2
+
3
+ module.exports = addPermissionWithRoles({
4
+ name: 'Read explore data',
5
+ action: 'read',
6
+ object: 'explore'
7
+ }, [
8
+ 'Administrator',
9
+ 'Admin Integration',
10
+ 'Ghost Explore Integration'
11
+ ]);
@@ -0,0 +1,38 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {default: ObjectID} = require('bson-objectid');
3
+ const {createTransactionalMigration, meta} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Creating Ghost Explore Integration');
8
+ const existingIntegration = await knex('integrations').where({
9
+ type: 'internal',
10
+ name: 'Ghost Explore',
11
+ slug: 'ghost-explore'
12
+ }).first();
13
+
14
+ if (existingIntegration) {
15
+ logging.warn('Integration already exists, skipping');
16
+ return;
17
+ }
18
+
19
+ await knex('integrations').insert({
20
+ id: (new ObjectID()).toHexString(),
21
+ type: 'internal',
22
+ name: 'Ghost Explore',
23
+ description: 'Internal Integration for the Ghost Explore directory',
24
+ slug: 'ghost-explore',
25
+ created_at: knex.raw('current_timestamp'),
26
+ created_by: meta.MIGRATION_USER
27
+ });
28
+ },
29
+ async function down(knex) {
30
+ logging.info('Deleting Ghost Explore Integration');
31
+
32
+ await knex('integrations').where({
33
+ type: 'internal',
34
+ name: 'Ghost Explore',
35
+ slug: 'ghost-explore'
36
+ }).del();
37
+ }
38
+ );
@@ -0,0 +1,73 @@
1
+ const {InternalServerError} = require('@tryghost/errors');
2
+ const logging = require('@tryghost/logging');
3
+ const security = require('@tryghost/security');
4
+ const {default: ObjectID} = require('bson-objectid');
5
+ const {createTransactionalMigration, meta} = require('../../utils');
6
+
7
+ module.exports = createTransactionalMigration(
8
+ async function up(knex) {
9
+ logging.info('Adding Admin API key for Ghost Explore Integration');
10
+
11
+ const integration = await knex('integrations').where({
12
+ slug: 'ghost-explore',
13
+ type: 'internal',
14
+ name: 'Ghost Explore'
15
+ }).first();
16
+
17
+ if (!integration) {
18
+ throw new InternalServerError('Could not find Ghost Explore Integration');
19
+ }
20
+
21
+ const role = await knex('roles').where({
22
+ name: 'Ghost Explore Integration'
23
+ }).first();
24
+
25
+ if (!role) {
26
+ throw new InternalServerError('Could not find Ghost Explore Integration Role');
27
+ }
28
+
29
+ const existingKey = await knex('api_keys').where({
30
+ integration_id: integration.id,
31
+ role_id: role.id
32
+ }).first();
33
+
34
+ if (existingKey) {
35
+ logging.warn('Admin API key already exists');
36
+ return;
37
+ }
38
+
39
+ await knex('api_keys').insert({
40
+ id: (new ObjectID()).toHexString(),
41
+ type: 'admin',
42
+ secret: security.secret.create('admin'),
43
+ role_id: role.id,
44
+ integration_id: integration.id,
45
+ created_at: knex.raw('current_timestamp'),
46
+ created_by: meta.MIGRATION_USER
47
+ });
48
+ },
49
+ async function down(knex) {
50
+ logging.info('Removing Ghost Explore API key');
51
+
52
+ const integration = await knex('integrations').where({
53
+ slug: 'ghost-explore',
54
+ type: 'internal',
55
+ name: 'Ghost Explore'
56
+ }).first();
57
+
58
+ const role = await knex('roles').where({
59
+ name: 'Ghost Explore Integration'
60
+ }).first();
61
+
62
+ if (!role || !integration) {
63
+ logging.warn('Could not delete API key');
64
+ return;
65
+ }
66
+
67
+ logging.info('Deleting API Key');
68
+ await knex('api_keys').where({
69
+ integration_id: integration.id,
70
+ role_id: role.id
71
+ }).del();
72
+ }
73
+ );
@@ -450,5 +450,19 @@
450
450
  "defaultValue": "all",
451
451
  "type": "string"
452
452
  }
453
+ },
454
+ "comments": {
455
+ "comments_enabled": {
456
+ "type": "string",
457
+ "defaultValue": "off",
458
+ "validations": {
459
+ "isEmpty": false,
460
+ "isIn": [[
461
+ "off",
462
+ "all",
463
+ "paid"
464
+ ]]
465
+ }
466
+ }
453
467
  }
454
468
  }
@@ -69,6 +69,10 @@
69
69
  "name": "Admin Integration",
70
70
  "description": "External Apps"
71
71
  },
72
+ {
73
+ "name": "Ghost Explore Integration",
74
+ "description": "Internal Integration for the Ghost Explore directory"
75
+ },
72
76
  {
73
77
  "name": "DB Backup Integration",
74
78
  "description": "Internal DB Backup Client"
@@ -82,6 +86,11 @@
82
86
  {
83
87
  "name": "Permission",
84
88
  "entries": [
89
+ {
90
+ "name": "Read explore data",
91
+ "action_type": "read",
92
+ "object_type": "explore"
93
+ },
85
94
  {
86
95
  "name": "Export database",
87
96
  "action_type": "exportContent",
@@ -561,6 +570,46 @@
561
570
  "name": "Edit newsletters",
562
571
  "action_type": "edit",
563
572
  "object_type": "newsletter"
573
+ },
574
+ {
575
+ "name": "Browse comments",
576
+ "action_type": "browse",
577
+ "object_type": "comment"
578
+ },
579
+ {
580
+ "name": "Read comments",
581
+ "action_type": "read",
582
+ "object_type": "comment"
583
+ },
584
+ {
585
+ "name": "Edit comments",
586
+ "action_type": "edit",
587
+ "object_type": "comment"
588
+ },
589
+ {
590
+ "name": "Add comments",
591
+ "action_type": "add",
592
+ "object_type": "comment"
593
+ },
594
+ {
595
+ "name": "Delete comments",
596
+ "action_type": "destroy",
597
+ "object_type": "comment"
598
+ },
599
+ {
600
+ "name": "Moderate comments",
601
+ "action_type": "moderate",
602
+ "object_type": "comment"
603
+ },
604
+ {
605
+ "name": "Like comments",
606
+ "action_type": "like",
607
+ "object_type": "comment"
608
+ },
609
+ {
610
+ "name": "Unlike comments",
611
+ "action_type": "unlike",
612
+ "object_type": "comment"
564
613
  }
565
614
  ]
566
615
  },
@@ -614,6 +663,13 @@
614
663
  "type": "builtin",
615
664
  "api_keys": [{"type": "admin"}]
616
665
  },
666
+ {
667
+ "slug": "ghost-explore",
668
+ "name": "Ghost Explore",
669
+ "description": "Built-in Ghost Explore integration",
670
+ "type": "internal",
671
+ "api_keys": [{"type": "admin", "role": "Ghost Explore Integration"}]
672
+ },
617
673
  {
618
674
  "slug": "ghost-backup",
619
675
  "name": "Ghost Backup",
@@ -678,7 +734,9 @@
678
734
  "offer": "all",
679
735
  "authentication": "resetAllPasswords",
680
736
  "members_stripe_connect": "auth",
681
- "newsletter": "all"
737
+ "newsletter": "all",
738
+ "explore": "read",
739
+ "comment": "all"
682
740
  },
683
741
  "DB Backup Integration": {
684
742
  "db": "all"
@@ -686,6 +744,9 @@
686
744
  "Scheduler Integration": {
687
745
  "post": "publish"
688
746
  },
747
+ "Ghost Explore Integration": {
748
+ "explore": "read"
749
+ },
689
750
  "Admin Integration": {
690
751
  "mail": "all",
691
752
  "notification": "all",
@@ -707,7 +768,9 @@
707
768
  "snippet": "all",
708
769
  "product": ["browse", "read", "add", "edit"],
709
770
  "offer": ["browse", "read", "add", "edit"],
710
- "newsletter": ["browse", "read", "add", "edit"]
771
+ "newsletter": ["browse", "read", "add", "edit"],
772
+ "explore": "read",
773
+ "comment": "all"
711
774
  },
712
775
  "Editor": {
713
776
  "notification": "all",
@@ -148,6 +148,7 @@ module.exports = {
148
148
  meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}},
149
149
  tour: {type: 'text', maxlength: 65535, nullable: true},
150
150
  last_seen: {type: 'dateTime', nullable: true},
151
+ comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
151
152
  created_at: {type: 'dateTime', nullable: false},
152
153
  created_by: {type: 'string', maxlength: 24, nullable: false},
153
154
  updated_at: {type: 'dateTime', nullable: true},
@@ -392,12 +393,15 @@ module.exports = {
392
393
  }
393
394
  },
394
395
  name: {type: 'string', maxlength: 191, nullable: true},
396
+ bio: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 50}}},
395
397
  note: {type: 'string', maxlength: 2000, nullable: true},
396
398
  geolocation: {type: 'string', maxlength: 2000, nullable: true},
399
+ enable_comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
397
400
  email_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
398
401
  email_opened_count: {type: 'integer', unsigned: true, nullable: false, defaultTo: 0},
399
402
  email_open_rate: {type: 'integer', unsigned: true, nullable: true, index: true},
400
- last_seen_at: {type: 'dateTime',nullable: true},
403
+ last_seen_at: {type: 'dateTime', nullable: true},
404
+ last_commented_at: {type: 'dateTime', nullable: true},
401
405
  created_at: {type: 'dateTime', nullable: false},
402
406
  created_by: {type: 'string', maxlength: 24, nullable: false},
403
407
  updated_at: {type: 'dateTime', nullable: true},
@@ -742,5 +746,31 @@ module.exports = {
742
746
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
743
747
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
744
748
  newsletter_id: {type: 'string', maxlength: 24, nullable: false, references: 'newsletters.id', cascadeDelete: true}
749
+ },
750
+ comments: {
751
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
752
+ post_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'posts.id', cascadeDelete: true},
753
+ member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id'},
754
+ parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id'},
755
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'published', validations: {isIn: [['published', 'hidden', 'deleted']]}},
756
+ html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
757
+ edited_at: {type: 'dateTime', nullable: true},
758
+ created_at: {type: 'dateTime', nullable: false},
759
+ updated_at: {type: 'dateTime', nullable: false}
760
+ },
761
+ comment_likes: {
762
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
763
+ comment_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'comments.id', cascadeDelete: true},
764
+ member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id'},
765
+ created_at: {type: 'dateTime', nullable: false},
766
+ updated_at: {type: 'dateTime', nullable: false}
767
+ },
768
+ comment_reports: {
769
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
770
+ comment_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'comments.id', cascadeDelete: true},
771
+ member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id'},
772
+ reason: {type: 'text', maxlength: 65535, nullable: false},
773
+ created_at: {type: 'dateTime', nullable: false},
774
+ updated_at: {type: 'dateTime', nullable: false}
745
775
  }
746
776
  };
@@ -7,6 +7,16 @@ const messages = {
7
7
  couldNotUnderstandRequest: 'Could not understand request.'
8
8
  };
9
9
 
10
+ // If user requested an excerpt we need to ensure plaintext and custom_excerpt is also included so we can include it when we query the database.
11
+ const requiredForExcerpt = (requestedColumns) => {
12
+ if (requestedColumns){
13
+ if (requestedColumns.includes('excerpt') && !requestedColumns.includes('plaintext') && !requestedColumns.includes('plaintext') || !requestedColumns) {
14
+ requestedColumns.push('plaintext');
15
+ requestedColumns.push('custom_excerpt');
16
+ }
17
+ }
18
+ };
19
+
10
20
  /**
11
21
  * @param {Bookshelf} Bookshelf
12
22
  */
@@ -72,6 +82,8 @@ module.exports = function (Bookshelf) {
72
82
  const options = this.filterOptions(unfilteredOptions, 'findPage');
73
83
  const itemCollection = this.getFilteredCollection(options);
74
84
  const requestedColumns = options.columns;
85
+ // make sure we include plaintext and custom_excerpt if excerpt is requested
86
+ requiredForExcerpt(requestedColumns);
75
87
 
76
88
  // Set this to true or pass ?debug=true as an API option to get output
77
89
  itemCollection.debug = unfilteredOptions.debug && process.env.NODE_ENV !== 'production';
@@ -126,6 +138,9 @@ module.exports = function (Bookshelf) {
126
138
  const options = this.filterOptions(unfilteredOptions, 'findOne');
127
139
  data = this.filterData(data);
128
140
  const model = this.forge(data);
141
+ const requestedColumns = options.columns;
142
+ // make sure we include plaintext and custom_excerpt if excerpt is requested
143
+ requiredForExcerpt(requestedColumns);
129
144
 
130
145
  // @NOTE: The API layer decides if this option is allowed
131
146
  if (options.filter) {
@@ -0,0 +1,34 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const CommentLike = ghostBookshelf.Model.extend({
4
+ tableName: 'comment_likes',
5
+
6
+ defaults: function defaults() {
7
+ return {};
8
+ },
9
+
10
+ comment() {
11
+ return this.belongsTo('Comment', 'comment_id');
12
+ },
13
+
14
+ member() {
15
+ return this.belongsTo('Member', 'member_id');
16
+ },
17
+
18
+ emitChange: function emitChange(event, options) {
19
+ const eventToTrigger = 'comment_like' + '.' + event;
20
+ ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
21
+ },
22
+
23
+ onCreated: function onCreated(model, options) {
24
+ ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
25
+
26
+ model.emitChange('added', options);
27
+ }
28
+ }, {
29
+
30
+ });
31
+
32
+ module.exports = {
33
+ CommentLike: ghostBookshelf.model('CommentLike', CommentLike)
34
+ };
@@ -0,0 +1,163 @@
1
+ const ghostBookshelf = require('./base');
2
+ const _ = require('lodash');
3
+ const errors = require('@tryghost/errors');
4
+ const tpl = require('@tryghost/tpl');
5
+ const commentsService = require('../services/comments');
6
+
7
+ const messages = {
8
+ commentNotFound: 'Comment could not be found',
9
+ notYourCommentToEdit: 'You may only edit your own comments',
10
+ notYourCommentToDestroy: 'You may only delete your own comments'
11
+ };
12
+
13
+ const Comment = ghostBookshelf.Model.extend({
14
+ tableName: 'comments',
15
+
16
+ defaults: function defaults() {
17
+ return {
18
+ status: 'published'
19
+ };
20
+ },
21
+
22
+ post() {
23
+ return this.belongsTo('Post', 'post_id');
24
+ },
25
+
26
+ member() {
27
+ return this.belongsTo('Member', 'member_id');
28
+ },
29
+
30
+ parent() {
31
+ return this.belongsTo('Comment', 'parent_id');
32
+ },
33
+
34
+ likes() {
35
+ return this.hasMany('CommentLike', 'comment_id');
36
+ },
37
+
38
+ replies() {
39
+ return this.hasMany('Comment', 'parent_id');
40
+ },
41
+
42
+ emitChange: function emitChange(event, options) {
43
+ const eventToTrigger = 'comment' + '.' + event;
44
+ ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
45
+ },
46
+
47
+ onSaving() {
48
+ ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
49
+
50
+ if (this.hasChanged('html')) {
51
+ const sanitizeHtml = require('sanitize-html');
52
+
53
+ this.set('html', sanitizeHtml(this.get('html'), {
54
+ allowedTags: ['p', 'br', 'a', 'blockquote'],
55
+ allowedAttributes: {
56
+ a: ['href', 'target', 'rel']
57
+ },
58
+ selfClosing: ['br'],
59
+ // Enforce _blank and safe URLs
60
+ transformTags: {
61
+ a: sanitizeHtml.simpleTransform('a', {
62
+ target: '_blank',
63
+ rel: 'ugc noopener noreferrer nofollow'
64
+ })
65
+ }
66
+ }));
67
+ }
68
+ },
69
+
70
+ onCreated: function onCreated(model, options) {
71
+ ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
72
+
73
+ if (!options.context.internal) {
74
+ commentsService.api.sendNewCommentNotifications(model);
75
+ }
76
+
77
+ model.emitChange('added', options);
78
+ },
79
+
80
+ enforcedFilters: function enforcedFilters() {
81
+ return 'parent_id:null';
82
+ }
83
+
84
+ }, {
85
+ destroy: function destroy(unfilteredOptions) {
86
+ let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
87
+
88
+ const softDelete = () => {
89
+ return ghostBookshelf.Model.edit.call(this, {status: 'deleted'}, options);
90
+ };
91
+
92
+ if (!options.transacting) {
93
+ return ghostBookshelf.transaction((transacting) => {
94
+ options.transacting = transacting;
95
+ return softDelete();
96
+ });
97
+ }
98
+
99
+ return softDelete();
100
+ },
101
+
102
+ async permissible(commentModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission, hasMemberPermission) {
103
+ const self = this;
104
+
105
+ if (hasUserPermission) {
106
+ return true;
107
+ }
108
+
109
+ if (_.isString(commentModelOrId)) {
110
+ // Grab the original args without the first one
111
+ const origArgs = _.toArray(arguments).slice(1);
112
+
113
+ // Get the actual comment model
114
+ return this.findOne({
115
+ id: commentModelOrId
116
+ }).then(function then(foundCommentModel) {
117
+ if (!foundCommentModel) {
118
+ throw new errors.NotFoundError({
119
+ message: tpl(messages.commentNotFound)
120
+ });
121
+ }
122
+
123
+ // Build up the original args but substitute with actual model
124
+ const newArgs = [foundCommentModel].concat(origArgs);
125
+
126
+ return self.permissible.apply(self, newArgs);
127
+ });
128
+ }
129
+
130
+ if (action === 'edit' && commentModelOrId.get('member_id') !== context.member.id) {
131
+ return Promise.reject(new errors.NoPermissionError({
132
+ message: tpl(messages.notYourCommentToEdit)
133
+ }));
134
+ }
135
+
136
+ if (action === 'destroy' && commentModelOrId.get('member_id') !== context.member.id) {
137
+ return Promise.reject(new errors.NoPermissionError({
138
+ message: tpl(messages.notYourCommentToDestroy)
139
+ }));
140
+ }
141
+
142
+ return hasMemberPermission;
143
+ },
144
+
145
+ /**
146
+ * We have to ensure consistency. If you listen on model events (e.g. `member.added`), you can expect that you always
147
+ * receive all fields including relations. Otherwise you can't rely on a consistent flow. And we want to avoid
148
+ * that event listeners have to re-fetch a resource. This function is used in the context of inserting
149
+ * and updating resources. We won't return the relations by default for now.
150
+ */
151
+ defaultRelations: function defaultRelations(methodName, options) {
152
+ // @todo: the default relations are not working for 'add' when we add it below
153
+ if (['findAll', 'findPage', 'edit', 'findOne'].indexOf(methodName) !== -1) {
154
+ options.withRelated = _.union(['member', 'likes', 'replies', 'replies.member', 'replies.likes'], options.withRelated || []);
155
+ }
156
+
157
+ return options;
158
+ }
159
+ });
160
+
161
+ module.exports = {
162
+ Comment: ghostBookshelf.model('Comment', Comment)
163
+ };
@@ -12,7 +12,8 @@ const Member = ghostBookshelf.Model.extend({
12
12
  status: 'free',
13
13
  uuid: uuid.v4(),
14
14
  email_count: 0,
15
- email_opened_count: 0
15
+ email_opened_count: 0,
16
+ enable_comment_notifications: true
16
17
  };
17
18
  },
18
19
 
@@ -61,7 +61,8 @@ User = ghostBookshelf.Model.extend({
61
61
  return {
62
62
  password: security.identifier.uid(50),
63
63
  visibility: 'public',
64
- status: 'active'
64
+ status: 'active',
65
+ comment_notifications: true
65
66
  };
66
67
  },
67
68
 
@@ -23,7 +23,7 @@ function getExpressSessionMiddleware() {
23
23
  maxAge: constants.SIX_MONTH_MS,
24
24
  httpOnly: true,
25
25
  path: urlUtils.getSubdir() + '/ghost',
26
- sameSite: 'lax',
26
+ sameSite: 'none',
27
27
  secure: urlUtils.isSSL(config.get('url'))
28
28
  }
29
29
  });