ghost 5.13.0 → 5.14.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 (121) hide show
  1. package/README.md +1 -1
  2. package/components/tryghost-adapter-manager-5.14.0.tgz +0 -0
  3. package/components/{tryghost-api-framework-5.13.0.tgz → tryghost-api-framework-5.14.0.tgz} +0 -0
  4. package/components/{tryghost-api-version-compatibility-service-5.13.0.tgz → tryghost-api-version-compatibility-service-5.14.0.tgz} +0 -0
  5. package/components/{tryghost-bootstrap-socket-5.13.0.tgz → tryghost-bootstrap-socket-5.14.0.tgz} +0 -0
  6. package/components/tryghost-constants-5.14.0.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.13.0.tgz → tryghost-custom-theme-settings-service-5.14.0.tgz} +0 -0
  8. package/components/tryghost-domain-events-5.14.0.tgz +0 -0
  9. package/components/{tryghost-email-analytics-provider-mailgun-5.13.0.tgz → tryghost-email-analytics-provider-mailgun-5.14.0.tgz} +0 -0
  10. package/components/tryghost-email-analytics-service-5.14.0.tgz +0 -0
  11. package/components/tryghost-email-content-generator-5.14.0.tgz +0 -0
  12. package/components/tryghost-express-dynamic-redirects-5.14.0.tgz +0 -0
  13. package/components/tryghost-extract-api-key-5.14.0.tgz +0 -0
  14. package/components/{tryghost-html-to-plaintext-5.13.0.tgz → tryghost-html-to-plaintext-5.14.0.tgz} +0 -0
  15. package/components/{tryghost-job-manager-5.13.0.tgz → tryghost-job-manager-5.14.0.tgz} +0 -0
  16. package/components/{tryghost-magic-link-5.13.0.tgz → tryghost-magic-link-5.14.0.tgz} +0 -0
  17. package/components/{tryghost-mailgun-client-5.13.0.tgz → tryghost-mailgun-client-5.14.0.tgz} +0 -0
  18. package/components/{tryghost-member-analytics-service-5.13.0.tgz → tryghost-member-analytics-service-5.14.0.tgz} +0 -0
  19. package/components/tryghost-member-attribution-5.14.0.tgz +0 -0
  20. package/components/tryghost-member-events-5.14.0.tgz +0 -0
  21. package/components/tryghost-members-analytics-ingress-5.14.0.tgz +0 -0
  22. package/components/tryghost-members-api-5.14.0.tgz +0 -0
  23. package/components/{tryghost-members-csv-5.13.0.tgz → tryghost-members-csv-5.14.0.tgz} +0 -0
  24. package/components/tryghost-members-events-service-5.14.0.tgz +0 -0
  25. package/components/tryghost-members-importer-5.14.0.tgz +0 -0
  26. package/components/{tryghost-members-offers-5.13.0.tgz → tryghost-members-offers-5.14.0.tgz} +0 -0
  27. package/components/tryghost-members-payments-5.14.0.tgz +0 -0
  28. package/components/{tryghost-members-ssr-5.13.0.tgz → tryghost-members-ssr-5.14.0.tgz} +0 -0
  29. package/components/{tryghost-members-stripe-service-5.13.0.tgz → tryghost-members-stripe-service-5.14.0.tgz} +0 -0
  30. package/components/{tryghost-minifier-5.13.0.tgz → tryghost-minifier-5.14.0.tgz} +0 -0
  31. package/components/tryghost-mw-api-version-mismatch-5.14.0.tgz +0 -0
  32. package/components/tryghost-mw-cache-control-5.14.0.tgz +0 -0
  33. package/components/{tryghost-mw-error-handler-5.13.0.tgz → tryghost-mw-error-handler-5.14.0.tgz} +0 -0
  34. package/components/tryghost-mw-session-from-token-5.14.0.tgz +0 -0
  35. package/components/tryghost-mw-update-user-last-seen-5.14.0.tgz +0 -0
  36. package/components/tryghost-mw-vhost-5.14.0.tgz +0 -0
  37. package/components/{tryghost-oembed-service-5.13.0.tgz → tryghost-oembed-service-5.14.0.tgz} +0 -0
  38. package/components/{tryghost-package-json-5.13.0.tgz → tryghost-package-json-5.14.0.tgz} +0 -0
  39. package/components/tryghost-security-5.14.0.tgz +0 -0
  40. package/components/{tryghost-session-service-5.13.0.tgz → tryghost-session-service-5.14.0.tgz} +0 -0
  41. package/components/tryghost-settings-path-manager-5.14.0.tgz +0 -0
  42. package/components/tryghost-staff-service-5.14.0.tgz +0 -0
  43. package/components/tryghost-update-check-service-5.14.0.tgz +0 -0
  44. package/components/{tryghost-verification-trigger-5.13.0.tgz → tryghost-verification-trigger-5.14.0.tgz} +0 -0
  45. package/components/{tryghost-version-notifications-data-service-5.13.0.tgz → tryghost-version-notifications-data-service-5.14.0.tgz} +0 -0
  46. package/core/boot.js +2 -0
  47. package/core/built/admin/assets/{chunk.143.e96aad00fdf7196692c7.js → chunk.143.53c5e3490ffdae025d84.js} +6 -6
  48. package/core/built/admin/assets/{chunk.174.8b8a64726921ecfda41b.js → chunk.174.2edaa0869bfc2d88cf90.js} +173 -172
  49. package/core/built/admin/assets/{chunk.178.1188f8d61af173f8c246.js → chunk.178.a31590ec7388630cd0d0.js} +4 -4
  50. package/core/built/admin/assets/{chunk.763.9a285d7351e1f4415f8d.js → chunk.579.2de3f4300baf25f9a0db.js} +22 -28
  51. package/core/built/admin/assets/{chunk.763.9a285d7351e1f4415f8d.js.LICENSE.txt → chunk.579.2de3f4300baf25f9a0db.js.LICENSE.txt} +0 -0
  52. package/core/built/admin/assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css +1 -0
  53. package/core/built/admin/assets/{ghost-3203510c519d3195f1e71a34e9eecc59.js → ghost-84a4336c7d5c1f3fba00868b0a5237cd.js} +817 -915
  54. package/core/built/admin/assets/ghost-dark-13b669d50f494edf24d832b32ece2177.css +1 -0
  55. package/core/built/admin/assets/img/logos/orb-pink-3-a66abc6df2b6ab64d1459a6535b725cd.png +0 -0
  56. package/core/built/admin/assets/{vendor-ab4b5dfdf8b86f24d726115ac7de0980.js → vendor-22a37451d7619a2b641310ecbcca4c05.js} +84 -81
  57. package/core/built/admin/index.html +6 -6
  58. package/core/frontend/helpers/ghost_head.js +3 -2
  59. package/core/frontend/helpers/url.js +1 -1
  60. package/core/frontend/services/data/fetch-data.js +0 -1
  61. package/core/frontend/src/admin-auth/message-handler.js +4 -4
  62. package/core/server/adapters/cache/Memory.js +1 -1
  63. package/core/server/api/endpoints/comments-members.js +4 -64
  64. package/core/server/api/endpoints/utils/serializers/input/pages.js +3 -0
  65. package/core/server/api/endpoints/utils/serializers/input/posts.js +3 -0
  66. package/core/server/api/endpoints/utils/serializers/input/utils/clean.js +12 -0
  67. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +1 -1
  68. package/core/server/data/migrations/versions/5.14/2022-09-02-12-55-rename-members-bio-to-expertise.js +34 -0
  69. package/core/server/data/schema/schema.js +1 -1
  70. package/core/server/models/post.js +6 -0
  71. package/core/server/services/adapter-manager/index.js +3 -3
  72. package/core/server/services/adapter-manager/options-resolver.js +30 -9
  73. package/core/server/services/comments/controller.js +52 -1
  74. package/core/server/services/comments/email-templates/new-comment-reply.hbs +1 -1
  75. package/core/server/services/comments/email-templates/new-comment.hbs +1 -1
  76. package/core/server/services/comments/email-templates/report.hbs +1 -1
  77. package/core/server/services/comments/emails.js +3 -3
  78. package/core/server/services/comments/service.js +52 -0
  79. package/core/server/services/mega/post-email-serializer.js +371 -368
  80. package/core/server/services/mega/template.js +4 -8
  81. package/core/server/services/member-attribution/index.js +4 -18
  82. package/core/server/services/members/middleware.js +1 -1
  83. package/core/server/services/members/service.js +0 -12
  84. package/core/server/services/members/utils.js +1 -1
  85. package/core/server/services/members-events/index.js +40 -0
  86. package/core/server/services/nft-oembed.js +5 -3
  87. package/core/shared/config/defaults.json +4 -6
  88. package/core/shared/labs.js +1 -1
  89. package/core/shared/settings-cache/cache.js +1 -1
  90. package/package.json +96 -94
  91. package/yarn.lock +275 -207
  92. package/components/tryghost-adapter-manager-5.13.0.tgz +0 -0
  93. package/components/tryghost-constants-5.13.0.tgz +0 -0
  94. package/components/tryghost-domain-events-5.13.0.tgz +0 -0
  95. package/components/tryghost-email-analytics-service-5.13.0.tgz +0 -0
  96. package/components/tryghost-email-content-generator-5.13.0.tgz +0 -0
  97. package/components/tryghost-express-dynamic-redirects-5.13.0.tgz +0 -0
  98. package/components/tryghost-extract-api-key-5.13.0.tgz +0 -0
  99. package/components/tryghost-member-attribution-5.13.0.tgz +0 -0
  100. package/components/tryghost-member-events-5.13.0.tgz +0 -0
  101. package/components/tryghost-members-analytics-ingress-5.13.0.tgz +0 -0
  102. package/components/tryghost-members-api-5.13.0.tgz +0 -0
  103. package/components/tryghost-members-events-service-5.13.0.tgz +0 -0
  104. package/components/tryghost-members-importer-5.13.0.tgz +0 -0
  105. package/components/tryghost-members-payments-5.13.0.tgz +0 -0
  106. package/components/tryghost-mw-api-version-mismatch-5.13.0.tgz +0 -0
  107. package/components/tryghost-mw-cache-control-5.13.0.tgz +0 -0
  108. package/components/tryghost-mw-session-from-token-5.13.0.tgz +0 -0
  109. package/components/tryghost-mw-update-user-last-seen-5.13.0.tgz +0 -0
  110. package/components/tryghost-mw-vhost-5.13.0.tgz +0 -0
  111. package/components/tryghost-security-5.13.0.tgz +0 -0
  112. package/components/tryghost-settings-path-manager-5.13.0.tgz +0 -0
  113. package/components/tryghost-staff-service-5.13.0.tgz +0 -0
  114. package/components/tryghost-update-check-service-5.13.0.tgz +0 -0
  115. package/content/themes/casper/assets/built/portal.min.js +0 -3
  116. package/core/built/admin/assets/ghost-647c9a79282265a4d29bf273c44f72c0.css +0 -1
  117. package/core/built/admin/assets/ghost-dark-d84a2701166840b73bbbbe657879b65e.css +0 -1
  118. package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
  119. package/core/server/adapters/cache/Base.js +0 -12
  120. package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +0 -7
  121. package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +0 -7
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.13%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.14%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-733135cd6cbca8126c6fa223d63a5bf3.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-647c9a79282265a4d29bf273c44f72c0.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-40adc8310dcdd0be163cbf7b9d89c59a.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -53,9 +53,9 @@
53
53
 
54
54
  <div id="ember-basic-dropdown-wormhole"></div>
55
55
 
56
- <script src="assets/vendor-ab4b5dfdf8b86f24d726115ac7de0980.js"></script>
57
- <script src="assets/chunk.763.9a285d7351e1f4415f8d.js"></script>
58
- <script src="assets/chunk.143.e96aad00fdf7196692c7.js"></script>
59
- <script src="assets/ghost-3203510c519d3195f1e71a34e9eecc59.js"></script>
56
+ <script src="assets/vendor-22a37451d7619a2b641310ecbcca4c05.js"></script>
57
+ <script src="assets/chunk.579.2de3f4300baf25f9a0db.js"></script>
58
+ <script src="assets/chunk.143.53c5e3490ffdae025d84.js"></script>
59
+ <script src="assets/ghost-84a4336c7d5c1f3fba00868b0a5237cd.js"></script>
60
60
  </body>
61
61
  </html>
@@ -167,11 +167,12 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
167
167
  }
168
168
 
169
169
  head.push('<link rel="canonical" href="' + escapeExpression(meta.canonicalUrl) + '" />');
170
- head.push('<meta name="referrer" content="' + referrerPolicy + '" />');
171
170
 
172
- // don't allow indexing of preview URLs!
173
171
  if (_.includes(context, 'preview')) {
174
172
  head.push(writeMetaTag('robots', 'noindex,nofollow', 'name'));
173
+ head.push(writeMetaTag('referrer', 'same-origin', 'name'));
174
+ } else {
175
+ head.push(writeMetaTag('referrer', referrerPolicy, 'name'));
175
176
  }
176
177
 
177
178
  // show amp link in post when 1. we are not on the amp page and 2. amp is enabled
@@ -17,7 +17,7 @@ module.exports = function url(options) {
17
17
  let outputUrl = getMetaDataUrl(this, absolute);
18
18
 
19
19
  try {
20
- outputUrl = encodeURI(decodeURI(outputUrl));
20
+ outputUrl = encodeURI(decodeURI(outputUrl)).replace(/%5B/g, '[').replace(/%5D/g, ']');
21
21
  } catch (err) {
22
22
  // Happens when the outputURL contains an invalid URI character like "%%" or "%80"
23
23
 
@@ -6,7 +6,6 @@ const _ = require('lodash');
6
6
  const Promise = require('bluebird');
7
7
 
8
8
  // The default settings for a default post query
9
- // @TODO: get rid of this config and use v2, v3 config
10
9
  const queryDefaults = {
11
10
  type: 'browse',
12
11
  resource: 'posts',
@@ -1,4 +1,4 @@
1
- const adminUrl = window.location.href.replace('auth-frame/', '');
1
+ const adminUrl = window.location.href.replace('auth-frame/', '') + 'api/admin';
2
2
 
3
3
  // At compile time, we'll replace the value with the actual origin.
4
4
  const siteOrigin = '{{SITE_ORIGIN}}';
@@ -26,7 +26,7 @@ window.addEventListener('message', async function (event) {
26
26
  if (data.action === 'getUser') {
27
27
  try {
28
28
  const res = await fetch(
29
- adminUrl + 'api/canary/admin/users/me/'
29
+ adminUrl + '/users/me/'
30
30
  );
31
31
  const json = await res.json();
32
32
  respond(null, json);
@@ -37,7 +37,7 @@ window.addEventListener('message', async function (event) {
37
37
 
38
38
  if (data.action === 'hideComment') {
39
39
  try {
40
- const res = await fetch(adminUrl + 'api/canary/admin/comments/' + data.id + '/', {
40
+ const res = await fetch(adminUrl + '/comments/' + data.id + '/', {
41
41
  method: 'PUT',
42
42
  body: JSON.stringify({
43
43
  comments: [{
@@ -58,7 +58,7 @@ window.addEventListener('message', async function (event) {
58
58
 
59
59
  if (data.action === 'showComment') {
60
60
  try {
61
- const res = await fetch(adminUrl + 'api/canary/admin/comments/' + data.id + '/', {
61
+ const res = await fetch(adminUrl + '/comments/' + data.id + '/', {
62
62
  method: 'PUT',
63
63
  body: JSON.stringify({
64
64
  comments: [{
@@ -1,4 +1,4 @@
1
- const Base = require('./Base');
1
+ const Base = require('@tryghost/adapter-base-cache');
2
2
 
3
3
  class MemoryCache extends Base {
4
4
  constructor() {
@@ -1,18 +1,7 @@
1
- const Promise = require('bluebird');
2
- const tpl = require('@tryghost/tpl');
3
- const errors = require('@tryghost/errors');
4
- const models = require('../../models');
5
1
  const commentsService = require('../../services/comments');
6
2
  const ALLOWED_INCLUDES = ['member', 'replies', 'replies.member', 'replies.count.likes', 'replies.liked', 'count.replies', 'count.likes', 'liked', 'post', 'parent'];
7
3
  const UNSAFE_ATTRS = ['status'];
8
4
 
9
- const messages = {
10
- commentNotFound: 'Comment could not be found',
11
- memberNotFound: 'Unable to find member',
12
- likeNotFound: 'Unable to find like',
13
- alreadyLiked: 'This comment was liked already'
14
- };
15
-
16
5
  module.exports = {
17
6
  docName: 'comments',
18
7
 
@@ -157,27 +146,7 @@ module.exports = {
157
146
  },
158
147
  permissions: true,
159
148
  async query(frame) {
160
- // TODO: move to likes service
161
- if (frame.options?.context?.member?.id) {
162
- const data = {
163
- member_id: frame.options.context.member.id,
164
- comment_id: frame.options.id
165
- };
166
-
167
- const existing = await models.CommentLike.findOne(data, frame.options);
168
-
169
- if (existing) {
170
- throw new errors.BadRequestError({
171
- message: tpl(messages.alreadyLiked)
172
- });
173
- }
174
-
175
- return await models.CommentLike.add(data, frame.options);
176
- } else {
177
- throw new errors.NotFoundError({
178
- message: tpl(messages.memberNotFound)
179
- });
180
- }
149
+ return await commentsService.controller.like(frame);
181
150
  }
182
151
  },
183
152
 
@@ -188,31 +157,8 @@ module.exports = {
188
157
  ],
189
158
  validation: {},
190
159
  permissions: true,
191
- query(frame) {
192
- // TODO: move to likes service
193
- if (frame.options?.context?.member?.id) {
194
- return models.CommentLike.destroy({
195
- ...frame.options,
196
- destroyBy: {
197
- member_id: frame.options.context.member.id,
198
- comment_id: frame.options.id
199
- },
200
- require: true
201
- }).then(() => null)
202
- .catch((err) => {
203
- if (err instanceof models.CommentLike.NotFoundError) {
204
- return Promise.reject(new errors.NotFoundError({
205
- message: tpl(messages.likeNotFound)
206
- }));
207
- }
208
-
209
- throw err;
210
- });
211
- } else {
212
- return Promise.reject(new errors.NotFoundError({
213
- message: tpl(messages.memberNotFound)
214
- }));
215
- }
160
+ async query(frame) {
161
+ return await commentsService.controller.unlike(frame);
216
162
  }
217
163
  },
218
164
 
@@ -224,13 +170,7 @@ module.exports = {
224
170
  validation: {},
225
171
  permissions: true,
226
172
  async query(frame) {
227
- if (!frame.options?.context?.member?.id) {
228
- return Promise.reject(new errors.UnauthorizedError({
229
- message: tpl(messages.memberNotFound)
230
- }));
231
- }
232
-
233
- await commentsService.api.reportComment(frame.options.id, frame.options?.context?.member);
173
+ await commentsService.controller.report(frame);
234
174
  }
235
175
  }
236
176
  };
@@ -5,6 +5,7 @@ const url = require('./utils/url');
5
5
  const slugFilterOrder = require('./utils/slug-filter-order');
6
6
  const localUtils = require('../../index');
7
7
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
8
+ const clean = require('./utils/clean');
8
9
 
9
10
  function removeMobiledocFormat(frame) {
10
11
  if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
@@ -159,6 +160,8 @@ module.exports = {
159
160
  frame.data.pages[0].tags[index] = {
160
161
  name: tag
161
162
  };
163
+ } else {
164
+ frame.data.pages[0].tags[index] = clean.pagesTag(tag);
162
165
  }
163
166
  });
164
167
  }
@@ -5,6 +5,7 @@ const slugFilterOrder = require('./utils/slug-filter-order');
5
5
  const localUtils = require('../../index');
6
6
  const mobiledoc = require('../../../../../lib/mobiledoc');
7
7
  const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
8
+ const clean = require('./utils/clean');
8
9
 
9
10
  function removeMobiledocFormat(frame) {
10
11
  if (frame.options.formats && frame.options.formats.includes('mobiledoc')) {
@@ -175,6 +176,8 @@ module.exports = {
175
176
  frame.data.posts[0].tags[index] = {
176
177
  name: tag
177
178
  };
179
+ } else {
180
+ frame.data.posts[0].tags[index] = clean.postsTag(tag);
178
181
  }
179
182
  });
180
183
  }
@@ -0,0 +1,12 @@
1
+ const _ = require('lodash');
2
+
3
+ const tagRelation = (attrs) => {
4
+ return _.pick(attrs, [
5
+ 'id',
6
+ 'name',
7
+ 'slug'
8
+ ]);
9
+ };
10
+
11
+ module.exports.pagesTag = tagRelation;
12
+ module.exports.postsTag = tagRelation;
@@ -14,7 +14,7 @@ const memberFields = [
14
14
  'id',
15
15
  'uuid',
16
16
  'name',
17
- 'bio',
17
+ 'expertise',
18
18
  'avatar_image'
19
19
  ];
20
20
 
@@ -0,0 +1,34 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createNonTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createNonTransactionalMigration (
5
+ async function up(knex) {
6
+ // check if the column exists before trying to rename it
7
+ const hasBio = await knex.schema.hasColumn('members', 'bio');
8
+ const hasExpertise = await knex.schema.hasColumn('members', 'expertise');
9
+
10
+ if (hasBio && !hasExpertise) {
11
+ logging.info('Renaming members.bio to members.expertise');
12
+ await knex.schema.table('members', (table) => {
13
+ table.renameColumn('bio', 'expertise');
14
+ }
15
+ );
16
+ } else {
17
+ logging.info('members.bio does not exist, skipping rename');
18
+ }
19
+ },
20
+ async function down(knex) {
21
+ const hasBio = await knex.schema.hasColumn('members', 'bio');
22
+ const hasExpertise = await knex.schema.hasColumn('members', 'expertise');
23
+
24
+ if (hasExpertise && !hasBio) {
25
+ logging.info(`Renaming members.expertise to members.bio`);
26
+ await knex.schema.table('members', (table) => {
27
+ table.renameColumn('expertise', 'bio');
28
+ }
29
+ );
30
+ } else {
31
+ logging.warn('members.expertise does not exist, skipping');
32
+ }
33
+ }
34
+ );
@@ -396,7 +396,7 @@ module.exports = {
396
396
  }
397
397
  },
398
398
  name: {type: 'string', maxlength: 191, nullable: true},
399
- bio: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 50}}},
399
+ expertise: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 50}}},
400
400
  note: {type: 'string', maxlength: 2000, nullable: true},
401
401
  geolocation: {type: 'string', maxlength: 2000, nullable: true},
402
402
  enable_comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
@@ -794,6 +794,12 @@ Post = ghostBookshelf.Model.extend({
794
794
  });
795
795
  }
796
796
 
797
+ if (this.get('tiers')) {
798
+ this.set('tiers', this.get('tiers').map(t => ({
799
+ id: t.id
800
+ })));
801
+ }
802
+
797
803
  return sequence(ops);
798
804
  },
799
805
 
@@ -15,7 +15,7 @@ const adapterManager = new AdapterManager({
15
15
  adapterManager.registerAdapter('storage', require('ghost-storage-base'));
16
16
  adapterManager.registerAdapter('scheduling', require('../../adapters/scheduling/SchedulingBase'));
17
17
  adapterManager.registerAdapter('sso', require('../../adapters/sso/Base'));
18
- adapterManager.registerAdapter('cache', require('../../adapters/cache/Base'));
18
+ adapterManager.registerAdapter('cache', require('@tryghost/adapter-base-cache'));
19
19
 
20
20
  module.exports = {
21
21
  /**
@@ -26,8 +26,8 @@ module.exports = {
26
26
  getAdapter(name) {
27
27
  const adapterServiceConfig = getAdapterServiceConfig(config);
28
28
 
29
- const {adapterType, adapterName, adapterConfig} = resolveAdapterOptions(name, adapterServiceConfig);
29
+ const {adapterClassName, adapterConfig} = resolveAdapterOptions(name, adapterServiceConfig);
30
30
 
31
- return adapterManager.getAdapter(adapterType, adapterName, adapterConfig);
31
+ return adapterManager.getAdapter(name, adapterClassName, adapterConfig);
32
32
  }
33
33
  };
@@ -1,18 +1,39 @@
1
- module.exports = function resolveAdapterOptions(name, adapterServiceConfig) {
1
+ /**
2
+ * Resolves full configuration for an adapter. Combining base class configurations
3
+ * along with feature-specific ones
4
+ *
5
+ * @param {String} name
6
+ * @param {Object} adapterServiceConfig
7
+ *
8
+ * @returns {{adapterClassName: String, adapterConfig: Object}}
9
+ */
10
+ const resolveAdapterOptions = (name, adapterServiceConfig) => {
2
11
  const [adapterType, feature] = name.split(':');
3
12
  const adapterSettings = adapterServiceConfig[adapterType];
4
13
 
5
- let adapterName;
14
+ let adapterClassName;
6
15
  let adapterConfig;
7
16
 
8
- // CASE: load resource-specific adapter when there is an adapter feature name specified as well as custom feature config
9
- if (feature && adapterSettings[feature] && adapterSettings[adapterSettings[feature]]) {
10
- adapterName = adapterSettings[feature];
11
- adapterConfig = adapterSettings[adapterName];
17
+ const hasFeatureConfig = feature && adapterSettings[feature];
18
+ if (hasFeatureConfig && adapterSettings[adapterSettings[feature]]) {
19
+ // CASE: load resource-specific adapter when there is an adapter feature
20
+ // name (String) specified as well as custom feature config
21
+ adapterClassName = adapterSettings[feature];
22
+ adapterConfig = adapterSettings[adapterClassName];
23
+ } else if (hasFeatureConfig && adapterSettings[feature].adapter) {
24
+ // CASE: load resource-specific adapter when there is an adapter feature
25
+ // name (Object) specified as well as custom feature config
26
+ adapterClassName = adapterSettings[feature].adapter;
27
+ const commonAdapterConfig = {...adapterSettings[adapterClassName]};
28
+ const featureAdapterConfig = {...adapterSettings[feature]};
29
+ delete featureAdapterConfig.adapter;
30
+ adapterConfig = {...commonAdapterConfig, ...featureAdapterConfig};
12
31
  } else {
13
- adapterName = adapterSettings.active;
14
- adapterConfig = adapterSettings[adapterName];
32
+ adapterClassName = adapterSettings.active;
33
+ adapterConfig = adapterSettings[adapterClassName];
15
34
  }
16
35
 
17
- return {adapterType, adapterName, adapterConfig};
36
+ return {adapterClassName, adapterConfig};
18
37
  };
38
+
39
+ module.exports = resolveAdapterOptions;
@@ -1,4 +1,5 @@
1
1
  const _ = require('lodash');
2
+ const errors = require('@tryghost/errors');
2
3
 
3
4
  /**
4
5
  * @typedef {import('../../api/shared/frame')} Frame
@@ -8,7 +9,8 @@ const {MethodNotAllowedError} = require('@tryghost/errors');
8
9
  const tpl = require('@tryghost/tpl');
9
10
 
10
11
  const messages = {
11
- cannotDestroyComments: 'You cannot destroy comments.'
12
+ cannotDestroyComments: 'You cannot destroy comments.',
13
+ memberNotFound: 'Unable to find member'
12
14
  };
13
15
 
14
16
  module.exports = class CommentsController {
@@ -21,6 +23,14 @@ module.exports = class CommentsController {
21
23
  this.stats = stats;
22
24
  }
23
25
 
26
+ #checkMember(frame) {
27
+ if (!frame.options?.context?.member?.id) {
28
+ throw new errors.UnauthorizedError({
29
+ message: tpl(messages.memberNotFound)
30
+ });
31
+ }
32
+ }
33
+
24
34
  /**
25
35
  * @param {Frame} frame
26
36
  */
@@ -46,6 +56,8 @@ module.exports = class CommentsController {
46
56
  * @param {Frame} frame
47
57
  */
48
58
  async edit(frame) {
59
+ this.#checkMember(frame);
60
+
49
61
  if (frame.data.comments[0].status === 'deleted') {
50
62
  return await this.service.deleteComment(
51
63
  frame.options.id,
@@ -66,6 +78,7 @@ module.exports = class CommentsController {
66
78
  * @param {Frame} frame
67
79
  */
68
80
  async add(frame) {
81
+ this.#checkMember(frame);
69
82
  const data = frame.data.comments[0];
70
83
 
71
84
  if (data.parent_id) {
@@ -98,4 +111,42 @@ module.exports = class CommentsController {
98
111
 
99
112
  return await this.stats.getCountsByPost(frame.data.ids);
100
113
  }
114
+
115
+ /**
116
+ * @param {Frame} frame
117
+ */
118
+ async like(frame) {
119
+ this.#checkMember(frame);
120
+
121
+ return await this.service.likeComment(
122
+ frame.options.id,
123
+ frame.options?.context?.member,
124
+ frame.options
125
+ );
126
+ }
127
+
128
+ /**
129
+ * @param {Frame} frame
130
+ */
131
+ async unlike(frame) {
132
+ this.#checkMember(frame);
133
+
134
+ return await this.service.unlikeComment(
135
+ frame.options.id,
136
+ frame.options?.context?.member,
137
+ frame.options
138
+ );
139
+ }
140
+
141
+ /**
142
+ * @param {Frame} frame
143
+ */
144
+ async report(frame) {
145
+ this.#checkMember(frame);
146
+
147
+ return await this.service.reportComment(
148
+ frame.options.id,
149
+ frame.options?.context?.member
150
+ );
151
+ }
101
152
  };
@@ -136,7 +136,7 @@
136
136
  </td>
137
137
  <td style="padding-right: 8px;">
138
138
  <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>
139
- <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}}{{replyDate}}</p>
139
+ <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 memberExpertise}}{{memberExpertise}} &#8226; {{/if}}{{replyDate}}</p>
140
140
  </td>
141
141
  </tr>
142
142
  </table>
@@ -136,7 +136,7 @@
136
136
  </td>
137
137
  <td style="padding-right: 8px;">
138
138
  <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>
139
- <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>
139
+ <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 memberExpertise}}{{memberExpertise}} &#8226; {{/if}}{{commentDate}}</p>
140
140
  </td>
141
141
  </tr>
142
142
  </table>
@@ -131,7 +131,7 @@
131
131
  </td>
132
132
  <td style="padding-right: 8px;">
133
133
  <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>
134
- <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>
134
+ <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 memberExpertise}}{{memberExpertise}} &#8226; {{/if}}{{commentDate}}</p>
135
135
  </td>
136
136
  </tr>
137
137
  </table>
@@ -41,7 +41,7 @@ class CommentsServiceEmails {
41
41
  commentHtml: comment.get('html'),
42
42
  commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
43
43
  memberName: memberName,
44
- memberBio: member.get('bio'),
44
+ memberExpertise: member.get('expertise'),
45
45
  memberInitials: this.extractInitials(memberName),
46
46
  accentColor: this.settingsCache.get('accent_color'),
47
47
  fromEmail: this.notificationFromAddress,
@@ -90,7 +90,7 @@ class CommentsServiceEmails {
90
90
  replyHtml: reply.get('html'),
91
91
  replyDate: moment(reply.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
92
92
  memberName: memberName,
93
- memberBio: member.get('bio'),
93
+ memberExpertise: member.get('expertise'),
94
94
  memberInitials: this.extractInitials(memberName),
95
95
  accentColor: this.settingsCache.get('accent_color'),
96
96
  fromEmail: this.notificationFromAddress,
@@ -140,7 +140,7 @@ class CommentsServiceEmails {
140
140
 
141
141
  memberName: memberName,
142
142
  memberEmail: member.get('email'),
143
- memberBio: member.get('bio'),
143
+ memberExpertise: member.get('expertise'),
144
144
  memberInitials: this.extractInitials(memberName),
145
145
  accentColor: this.settingsCache.get('accent_color'),
146
146
  fromEmail: this.notificationFromAddress,
@@ -87,6 +87,58 @@ class CommentsService {
87
87
  }
88
88
  }
89
89
 
90
+ async likeComment(commentId, member, options = {}) {
91
+ this.checkEnabled();
92
+
93
+ const memberModel = await this.models.Member.findOne({
94
+ id: member.id
95
+ }, {
96
+ require: true,
97
+ ...options,
98
+ withRelated: ['products']
99
+ });
100
+
101
+ this.checkCommentAccess(memberModel);
102
+
103
+ const data = {
104
+ member_id: memberModel.id,
105
+ comment_id: commentId
106
+ };
107
+
108
+ const existing = await this.models.CommentLike.findOne(data, options);
109
+
110
+ if (existing) {
111
+ throw new errors.BadRequestError({
112
+ message: tpl(messages.alreadyLiked)
113
+ });
114
+ }
115
+
116
+ return await this.models.CommentLike.add(data, options);
117
+ }
118
+
119
+ async unlikeComment(commentId, member, options = {}) {
120
+ this.checkEnabled();
121
+
122
+ try {
123
+ await this.models.CommentLike.destroy({
124
+ ...options,
125
+ destroyBy: {
126
+ member_id: member.id,
127
+ comment_id: commentId
128
+ },
129
+ require: true
130
+ });
131
+ } catch (err) {
132
+ if (err instanceof this.models.CommentLike.NotFoundError) {
133
+ return Promise.reject(new errors.NotFoundError({
134
+ message: tpl(messages.likeNotFound)
135
+ }));
136
+ }
137
+
138
+ throw err;
139
+ }
140
+ }
141
+
90
142
  async reportComment(commentId, reporter) {
91
143
  this.checkEnabled();
92
144
  const comment = await this.models.Comment.findOne({id: commentId}, {require: true});