ghost 5.64.0 → 5.65.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 (157) hide show
  1. package/components/{tryghost-adapter-cache-memory-ttl-5.64.0.tgz → tryghost-adapter-cache-memory-ttl-5.65.0.tgz} +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.64.0.tgz → tryghost-adapter-cache-redis-5.65.0.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.64.0.tgz → tryghost-adapter-manager-5.65.0.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.65.0.tgz +0 -0
  5. package/components/tryghost-api-framework-5.65.0.tgz +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.65.0.tgz +0 -0
  7. package/components/{tryghost-audience-feedback-5.64.0.tgz → tryghost-audience-feedback-5.65.0.tgz} +0 -0
  8. package/components/{tryghost-bookshelf-repository-5.64.0.tgz → tryghost-bookshelf-repository-5.65.0.tgz} +0 -0
  9. package/components/tryghost-bootstrap-socket-5.65.0.tgz +0 -0
  10. package/components/tryghost-collections-5.65.0.tgz +0 -0
  11. package/components/{tryghost-constants-5.64.0.tgz → tryghost-constants-5.65.0.tgz} +0 -0
  12. package/components/{tryghost-custom-theme-settings-service-5.64.0.tgz → tryghost-custom-theme-settings-service-5.65.0.tgz} +0 -0
  13. package/components/{tryghost-data-generator-5.64.0.tgz → tryghost-data-generator-5.65.0.tgz} +0 -0
  14. package/components/{tryghost-domain-events-5.64.0.tgz → tryghost-domain-events-5.65.0.tgz} +0 -0
  15. package/components/tryghost-donations-5.65.0.tgz +0 -0
  16. package/components/{tryghost-dynamic-routing-events-5.64.0.tgz → tryghost-dynamic-routing-events-5.65.0.tgz} +0 -0
  17. package/components/{tryghost-email-analytics-provider-mailgun-5.64.0.tgz → tryghost-email-analytics-provider-mailgun-5.65.0.tgz} +0 -0
  18. package/components/tryghost-email-analytics-service-5.65.0.tgz +0 -0
  19. package/components/{tryghost-email-content-generator-5.64.0.tgz → tryghost-email-content-generator-5.65.0.tgz} +0 -0
  20. package/components/tryghost-email-events-5.65.0.tgz +0 -0
  21. package/components/{tryghost-email-service-5.64.0.tgz → tryghost-email-service-5.65.0.tgz} +0 -0
  22. package/components/tryghost-email-suppression-list-5.65.0.tgz +0 -0
  23. package/components/tryghost-event-aware-cache-wrapper-5.65.0.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.64.0.tgz → tryghost-express-dynamic-redirects-5.65.0.tgz} +0 -0
  25. package/components/{tryghost-external-media-inliner-5.64.0.tgz → tryghost-external-media-inliner-5.65.0.tgz} +0 -0
  26. package/components/{tryghost-extract-api-key-5.64.0.tgz → tryghost-extract-api-key-5.65.0.tgz} +0 -0
  27. package/components/{tryghost-html-to-plaintext-5.64.0.tgz → tryghost-html-to-plaintext-5.65.0.tgz} +0 -0
  28. package/components/tryghost-i18n-5.65.0.tgz +0 -0
  29. package/components/tryghost-importer-handler-content-files-5.65.0.tgz +0 -0
  30. package/components/{tryghost-importer-revue-5.64.0.tgz → tryghost-importer-revue-5.65.0.tgz} +0 -0
  31. package/components/tryghost-in-memory-repository-5.65.0.tgz +0 -0
  32. package/components/{tryghost-job-manager-5.64.0.tgz → tryghost-job-manager-5.65.0.tgz} +0 -0
  33. package/components/{tryghost-link-redirects-5.64.0.tgz → tryghost-link-redirects-5.65.0.tgz} +0 -0
  34. package/components/tryghost-link-replacer-5.65.0.tgz +0 -0
  35. package/components/{tryghost-link-tracking-5.64.0.tgz → tryghost-link-tracking-5.65.0.tgz} +0 -0
  36. package/components/{tryghost-magic-link-5.64.0.tgz → tryghost-magic-link-5.65.0.tgz} +0 -0
  37. package/components/tryghost-mail-events-5.65.0.tgz +0 -0
  38. package/components/{tryghost-mailgun-client-5.64.0.tgz → tryghost-mailgun-client-5.65.0.tgz} +0 -0
  39. package/components/{tryghost-member-attribution-5.64.0.tgz → tryghost-member-attribution-5.65.0.tgz} +0 -0
  40. package/components/{tryghost-member-events-5.64.0.tgz → tryghost-member-events-5.65.0.tgz} +0 -0
  41. package/components/tryghost-members-api-5.65.0.tgz +0 -0
  42. package/components/{tryghost-members-csv-5.64.0.tgz → tryghost-members-csv-5.65.0.tgz} +0 -0
  43. package/components/{tryghost-members-events-service-5.64.0.tgz → tryghost-members-events-service-5.65.0.tgz} +0 -0
  44. package/components/{tryghost-members-importer-5.64.0.tgz → tryghost-members-importer-5.65.0.tgz} +0 -0
  45. package/components/{tryghost-members-offers-5.64.0.tgz → tryghost-members-offers-5.65.0.tgz} +0 -0
  46. package/components/tryghost-members-payments-5.65.0.tgz +0 -0
  47. package/components/tryghost-members-ssr-5.65.0.tgz +0 -0
  48. package/components/{tryghost-members-stripe-service-5.64.0.tgz → tryghost-members-stripe-service-5.65.0.tgz} +0 -0
  49. package/components/tryghost-mentions-email-report-5.65.0.tgz +0 -0
  50. package/components/{tryghost-milestones-5.64.0.tgz → tryghost-milestones-5.65.0.tgz} +0 -0
  51. package/components/tryghost-minifier-5.65.0.tgz +0 -0
  52. package/components/tryghost-model-to-domain-event-interceptor-5.65.0.tgz +0 -0
  53. package/components/tryghost-mw-api-version-mismatch-5.65.0.tgz +0 -0
  54. package/components/{tryghost-mw-cache-control-5.64.0.tgz → tryghost-mw-cache-control-5.65.0.tgz} +0 -0
  55. package/components/{tryghost-mw-error-handler-5.64.0.tgz → tryghost-mw-error-handler-5.65.0.tgz} +0 -0
  56. package/components/tryghost-mw-session-from-token-5.65.0.tgz +0 -0
  57. package/components/tryghost-mw-update-user-last-seen-5.65.0.tgz +0 -0
  58. package/components/{tryghost-mw-version-match-5.64.0.tgz → tryghost-mw-version-match-5.65.0.tgz} +0 -0
  59. package/components/tryghost-mw-vhost-5.65.0.tgz +0 -0
  60. package/components/tryghost-nql-filter-expansions-5.65.0.tgz +0 -0
  61. package/components/tryghost-oembed-service-5.65.0.tgz +0 -0
  62. package/components/{tryghost-package-json-5.64.0.tgz → tryghost-package-json-5.65.0.tgz} +0 -0
  63. package/components/{tryghost-post-events-5.64.0.tgz → tryghost-post-events-5.65.0.tgz} +0 -0
  64. package/components/{tryghost-post-revisions-5.64.0.tgz → tryghost-post-revisions-5.65.0.tgz} +0 -0
  65. package/components/tryghost-posts-service-5.65.0.tgz +0 -0
  66. package/components/tryghost-recommendations-5.65.0.tgz +0 -0
  67. package/components/tryghost-referrers-5.65.0.tgz +0 -0
  68. package/components/tryghost-security-5.65.0.tgz +0 -0
  69. package/components/tryghost-session-service-5.65.0.tgz +0 -0
  70. package/components/{tryghost-settings-path-manager-5.64.0.tgz → tryghost-settings-path-manager-5.65.0.tgz} +0 -0
  71. package/components/tryghost-slack-notifications-5.65.0.tgz +0 -0
  72. package/components/{tryghost-staff-service-5.64.0.tgz → tryghost-staff-service-5.65.0.tgz} +0 -0
  73. package/components/{tryghost-stats-service-5.64.0.tgz → tryghost-stats-service-5.65.0.tgz} +0 -0
  74. package/components/{tryghost-tiers-5.64.0.tgz → tryghost-tiers-5.65.0.tgz} +0 -0
  75. package/components/{tryghost-update-check-service-5.64.0.tgz → tryghost-update-check-service-5.65.0.tgz} +0 -0
  76. package/components/{tryghost-verification-trigger-5.64.0.tgz → tryghost-verification-trigger-5.65.0.tgz} +0 -0
  77. package/components/{tryghost-version-notifications-data-service-5.64.0.tgz → tryghost-version-notifications-data-service-5.65.0.tgz} +0 -0
  78. package/components/tryghost-webmentions-5.65.0.tgz +0 -0
  79. package/core/built/admin/assets/admin-x-settings/CodeEditorView-2b332315.mjs +2347 -0
  80. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +6 -0
  81. package/core/built/admin/assets/admin-x-settings/index-4a560e9e.mjs +19693 -0
  82. package/core/built/admin/assets/admin-x-settings/index-9cc1df42.mjs +13955 -0
  83. package/core/built/admin/assets/admin-x-settings/index-b8cc4f2e.mjs +3465 -0
  84. package/core/built/admin/assets/admin-x-settings/limit-service-7694c0d4.mjs +12902 -0
  85. package/core/built/admin/assets/admin-x-settings/modals-2073207d.mjs +11518 -0
  86. package/core/built/admin/assets/{chunk.143.ff0abfe4495f055b0f3d.js → chunk.143.c91dc1d2a8a5a1f318ec.js} +5 -5
  87. package/core/built/admin/assets/{chunk.178.3e470522312d6d4ed2e4.js → chunk.178.d921de78463d288f3bcd.js} +4 -4
  88. package/core/built/admin/assets/{chunk.237.9b7032162949850f6c76.js → chunk.518.396deed1a0c759d1f403.js} +1136 -1077
  89. package/core/built/admin/assets/{ghost-10ff4bcba99e1759098702a12c531352.js → ghost-38746349a72536a92f4d8235a582d716.js} +58 -57
  90. package/core/built/admin/assets/ghost-761b67711827277b7095ebc7acc5c49f.css +1 -0
  91. package/core/built/admin/assets/ghost-dark-d3f636c30e80bdd7f21d3fb4770073c9.css +1 -0
  92. package/core/built/admin/assets/{vendor-f8ce8bd43cf5dad6608f828ab48cee9b.js → vendor-240cd1dbd84a2e07709b867442c41840.js} +4 -4
  93. package/core/built/admin/index.html +6 -6
  94. package/core/frontend/src/cards/css/collection.css +42 -12
  95. package/core/server/api/endpoints/posts-public.js +63 -0
  96. package/core/server/api/endpoints/recommendations-public.js +1 -1
  97. package/core/server/api/endpoints/recommendations.js +20 -5
  98. package/core/server/api/endpoints/utils/serializers/output/config.js +0 -1
  99. package/core/server/api/endpoints/utils/serializers/output/site.js +1 -1
  100. package/core/server/data/migrations/versions/5.64/2023-09-19-04-25-40-truncate-stale-built-in-collections-posts.js +6 -8
  101. package/core/server/data/migrations/versions/5.64/2023-09-19-04-34-10-repopulate-built-in-collection-posts.js +6 -65
  102. package/core/server/data/migrations/versions/5.65/2023-09-22-06-42-15-truncate-stale-built-in-collections-posts.js +12 -0
  103. package/core/server/data/migrations/versions/5.65/2023-09-22-06-42-55-repopulate-built-in-featured-collection-posts.js +49 -0
  104. package/core/server/data/schema/fixtures/fixtures.json +0 -14
  105. package/core/server/lib/lexical.js +22 -3
  106. package/core/server/lib/request-external.js +10 -4
  107. package/core/server/models/post.js +71 -1
  108. package/core/server/models/relations/authors.js +1 -1
  109. package/core/server/services/collections/BookshelfCollectionsRepository.js +38 -35
  110. package/core/server/services/collections/PostsRepository.js +5 -0
  111. package/core/server/services/collections/service.js +7 -3
  112. package/core/server/services/members/content-gating.js +11 -7
  113. package/core/server/services/mentions/WebmentionMetadata.js +14 -2
  114. package/core/server/services/public-config/site.js +1 -1
  115. package/core/server/services/recommendations/RecommendationServiceWrapper.js +3 -2
  116. package/core/server/web/api/endpoints/admin/routes.js +2 -1
  117. package/core/shared/config/defaults.json +0 -4
  118. package/core/shared/labs.js +1 -1
  119. package/package.json +160 -159
  120. package/yarn.lock +361 -364
  121. package/components/tryghost-announcement-bar-settings-5.64.0.tgz +0 -0
  122. package/components/tryghost-api-framework-5.64.0.tgz +0 -0
  123. package/components/tryghost-api-version-compatibility-service-5.64.0.tgz +0 -0
  124. package/components/tryghost-bootstrap-socket-5.64.0.tgz +0 -0
  125. package/components/tryghost-collections-5.64.0.tgz +0 -0
  126. package/components/tryghost-donations-5.64.0.tgz +0 -0
  127. package/components/tryghost-email-analytics-service-5.64.0.tgz +0 -0
  128. package/components/tryghost-email-events-5.64.0.tgz +0 -0
  129. package/components/tryghost-email-suppression-list-5.64.0.tgz +0 -0
  130. package/components/tryghost-event-aware-cache-wrapper-5.64.0.tgz +0 -0
  131. package/components/tryghost-i18n-5.64.0.tgz +0 -0
  132. package/components/tryghost-importer-handler-content-files-5.64.0.tgz +0 -0
  133. package/components/tryghost-in-memory-repository-5.64.0.tgz +0 -0
  134. package/components/tryghost-link-replacer-5.64.0.tgz +0 -0
  135. package/components/tryghost-mail-events-5.64.0.tgz +0 -0
  136. package/components/tryghost-members-api-5.64.0.tgz +0 -0
  137. package/components/tryghost-members-payments-5.64.0.tgz +0 -0
  138. package/components/tryghost-members-ssr-5.64.0.tgz +0 -0
  139. package/components/tryghost-mentions-email-report-5.64.0.tgz +0 -0
  140. package/components/tryghost-minifier-5.64.0.tgz +0 -0
  141. package/components/tryghost-model-to-domain-event-interceptor-5.64.0.tgz +0 -0
  142. package/components/tryghost-mw-api-version-mismatch-5.64.0.tgz +0 -0
  143. package/components/tryghost-mw-session-from-token-5.64.0.tgz +0 -0
  144. package/components/tryghost-mw-update-user-last-seen-5.64.0.tgz +0 -0
  145. package/components/tryghost-mw-vhost-5.64.0.tgz +0 -0
  146. package/components/tryghost-nql-filter-expansions-5.64.0.tgz +0 -0
  147. package/components/tryghost-oembed-service-5.64.0.tgz +0 -0
  148. package/components/tryghost-posts-service-5.64.0.tgz +0 -0
  149. package/components/tryghost-recommendations-5.64.0.tgz +0 -0
  150. package/components/tryghost-referrers-5.64.0.tgz +0 -0
  151. package/components/tryghost-security-5.64.0.tgz +0 -0
  152. package/components/tryghost-session-service-5.64.0.tgz +0 -0
  153. package/components/tryghost-slack-notifications-5.64.0.tgz +0 -0
  154. package/components/tryghost-webmentions-5.64.0.tgz +0 -0
  155. package/core/built/admin/assets/ghost-33664cad4cd6664a8b5fa56e62c5005f.css +0 -1
  156. package/core/built/admin/assets/ghost-dark-0452daeaee3a9b16dcd954ea60dad518.css +0 -1
  157. /package/core/built/admin/assets/{chunk.237.9b7032162949850f6c76.js.LICENSE.txt → chunk.518.396deed1a0c759d1f403.js.LICENSE.txt} +0 -0
@@ -367,6 +367,47 @@ Post = ghostBookshelf.Model.extend({
367
367
  ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
368
368
  },
369
369
 
370
+ onFetched: async function onFetched(model, response, options) {
371
+ if (!labs.isSet('collectionsCard')) {
372
+ return;
373
+ }
374
+
375
+ await this.renderIfNeeded(model, options);
376
+ },
377
+
378
+ onFetchedCollection: async function onFetched(collection, response, options) {
379
+ if (!labs.isSet('collectionsCard')) {
380
+ return;
381
+ }
382
+
383
+ for await (const model of collection.models) {
384
+ await this.renderIfNeeded(model, options);
385
+ }
386
+ },
387
+
388
+ renderIfNeeded: async function renderIfNeeded(model, options = {}) {
389
+ // pages can have their html cleared to "queue" a re-render to update dynamic data such
390
+ // as collection cards. Detect that and re-render here so the page is always up to date
391
+ if (model.get('lexical') !== null && model.get('html') === null) {
392
+ const html = await lexicalLib.render(model.get('lexical'));
393
+ const plaintext = htmlToPlaintext.excerpt(html);
394
+
395
+ // set model attributes so they are available immediately in code that uses the returned model
396
+ model.set('html', html);
397
+ model.set('plaintext', plaintext);
398
+
399
+ // update database manually using knex to avoid hooks being called multiple times
400
+ const query = ghostBookshelf.knex.raw('UPDATE posts SET html = ?, plaintext = ? WHERE id = ?', [html, plaintext, model.id]);
401
+ if (options.transacting) {
402
+ await query.transacting(options.transacting);
403
+ } else {
404
+ await query;
405
+ }
406
+ }
407
+
408
+ return model;
409
+ },
410
+
370
411
  /**
371
412
  * We update the tags after the Post was inserted.
372
413
  * We update the tags before the Post was updated, see `onSaving` event.
@@ -464,9 +505,25 @@ Post = ghostBookshelf.Model.extend({
464
505
  }
465
506
  },
466
507
 
467
- onDestroyed: function onDestroyed(model, options) {
508
+ onDestroyed: async function onDestroyed(model, options) {
468
509
  ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);
469
510
 
511
+ if (labs.isSet('collectionsCard') && model.previous('type') === 'post' && model.previous('status') === 'published') {
512
+ // reset all page HTML when a published post is deleted so they can be re-rendered
513
+ // on next fetch so any collection cards are "dynamically" updated
514
+ const resetPages = function resetPages(transacting) {
515
+ return ghostBookshelf.knex.raw('UPDATE posts set html = NULL WHERE type = \'page\' AND lexical IS NOT NULL').transacting(transacting);
516
+ };
517
+
518
+ if (options.transacting) {
519
+ await resetPages(options.transacting);
520
+ } else {
521
+ await ghostBookshelf.knex.transaction(async (transacting) => {
522
+ await resetPages(transacting);
523
+ });
524
+ }
525
+ }
526
+
470
527
  if (model.previous('status') === 'published') {
471
528
  model.emitChange('unpublished', Object.assign({usePreviousAttribute: true}, options));
472
529
  }
@@ -941,6 +998,19 @@ Post = ghostBookshelf.Model.extend({
941
998
  })));
942
999
  }
943
1000
 
1001
+ if (labs.isSet('collectionsCard') && this.get('type') === 'post' && (newStatus === 'published' || olderStatus === 'published')) {
1002
+ // reset all page HTML when a published post is updated so they can be re-rendered
1003
+ // on next fetch so any collection cards are "dynamically" updated
1004
+ ops.push(async function resetPageHTML() {
1005
+ const query = ghostBookshelf.knex.raw('UPDATE posts set html = NULL WHERE type = ? AND lexical IS NOT NULL', ['page']);
1006
+ if (options.transacting) {
1007
+ await query.transacting(options.transacting);
1008
+ } else {
1009
+ await query;
1010
+ }
1011
+ });
1012
+ }
1013
+
944
1014
  return sequence(ops);
945
1015
  },
946
1016
 
@@ -71,7 +71,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
71
71
  model._originalOptions = collection._originalOptions;
72
72
  }));
73
73
 
74
- return proto.onFetchingCollection.call(this, collection, attrs, options);
74
+ return proto.onFetchedCollection.call(this, collection, attrs, options);
75
75
  },
76
76
 
77
77
  onCreating: function onCreating(model, attrs, options) {
@@ -12,9 +12,12 @@ const {default: ObjectID} = require('bson-objectid');
12
12
  module.exports = class BookshelfCollectionsRepository {
13
13
  #model;
14
14
  #relationModel;
15
- constructor(model, relationModel) {
15
+ /** @type {import('@tryghost/domain-events')} */
16
+ #DomainEvents;
17
+ constructor(model, relationModel, DomainEvents) {
16
18
  this.#model = model;
17
19
  this.#relationModel = relationModel;
20
+ this.#DomainEvents = DomainEvents;
18
21
  }
19
22
 
20
23
  async createTransaction(cb) {
@@ -201,46 +204,46 @@ module.exports = class BookshelfCollectionsRepository {
201
204
  transacting: options.transaction
202
205
  });
203
206
 
204
- const collectionPostsRelations = collection.posts.map((postId, index) => {
205
- return {
206
- id: (new ObjectID).toHexString(),
207
- sort_order: collection.type === 'manual' ? index : 0,
208
- collection_id: collection.id,
209
- post_id: postId
210
- };
211
- });
212
-
213
- const collectionPostRelationsToDeleteIds = [];
214
-
215
207
  if (collection.type === 'manual') {
208
+ const collectionPostsRelations = collection.posts.map((postId, index) => {
209
+ return {
210
+ id: (new ObjectID).toHexString(),
211
+ sort_order: index,
212
+ collection_id: collection.id,
213
+ post_id: postId
214
+ };
215
+ });
216
216
  await this.#relationModel.query().delete().where('collection_id', collection.id).transacting(options.transaction);
217
- } else {
218
- const collectionPostsOptions = {
219
- transaction: options.transaction,
220
- columns: ['id', 'post_id']
221
- };
222
- const existingRelations = await this.#fetchCollectionPostIds(collection.id, collectionPostsOptions);
223
-
224
- for (const existingRelation of existingRelations) {
225
- const found = collectionPostsRelations.find((thing) => {
226
- return thing.post_id === existingRelation.post_id;
227
- });
228
- if (found) {
229
- found.id = null;
230
- } else {
231
- collectionPostRelationsToDeleteIds.push(existingRelation.id);
232
- }
217
+ if (collectionPostsRelations.length > 0) {
218
+ await this.#relationModel.query().insert(collectionPostsRelations).transacting(options.transaction);
233
219
  }
234
- }
220
+ } else {
221
+ const collectionPostsToDelete = collection.events.filter(event => event.type === 'CollectionPostRemoved').map((event) => {
222
+ return event.data.post_id;
223
+ });
235
224
 
236
- const missingCollectionPostsRelations = collectionPostsRelations.filter(thing => thing.id !== null);
225
+ const collectionPostsToInsert = collection.events.filter(event => event.type === 'CollectionPostAdded').map((event) => {
226
+ return {
227
+ id: (new ObjectID).toHexString(),
228
+ sort_order: 0,
229
+ collection_id: collection.id,
230
+ post_id: event.data.post_id
231
+ };
232
+ });
237
233
 
238
- if (missingCollectionPostsRelations.length > 0) {
239
- await this.#relationModel.query().insert(missingCollectionPostsRelations).transacting(options.transaction);
240
- }
241
- if (collectionPostRelationsToDeleteIds.length > 0) {
242
- await this.#relationModel.query().delete().whereIn('id', collectionPostRelationsToDeleteIds).transacting(options.transaction);
234
+ if (collectionPostsToDelete.length > 0) {
235
+ await this.#relationModel.query().delete().where('collection_id', collection.id).whereIn('post_id', collectionPostsToDelete).transacting(options.transaction);
236
+ }
237
+ if (collectionPostsToInsert.length > 0) {
238
+ await this.#relationModel.query().insert(collectionPostsToInsert).transacting(options.transaction);
239
+ }
243
240
  }
241
+
242
+ options.transaction.executionPromise.then(() => {
243
+ for (const event of collection.events) {
244
+ this.#DomainEvents.dispatch(event);
245
+ }
246
+ });
244
247
  }
245
248
  }
246
249
  };
@@ -4,6 +4,11 @@ class PostsRepository {
4
4
  this.moment = moment;
5
5
  }
6
6
 
7
+ async getAllIds() {
8
+ const rows = await this.models.Post.query().select('id').where('type', 'post');
9
+
10
+ return rows.map(row => row.id);
11
+ }
7
12
  async getAll({filter, transaction}) {
8
13
  const {data: models} = await this.models.Post.findPage({
9
14
  filter: `(${filter})+type:post`,
@@ -12,7 +12,7 @@ class CollectionsServiceWrapper {
12
12
  const DomainEvents = require('@tryghost/domain-events');
13
13
  const postsRepository = require('./PostsRepository').getInstance();
14
14
  const models = require('../../models');
15
- const collectionsRepositoryInMemory = new BookshelfCollectionsRepository(models.Collection, models.CollectionPost);
15
+ const collectionsRepositoryInMemory = new BookshelfCollectionsRepository(models.Collection, models.CollectionPost, DomainEvents);
16
16
 
17
17
  const collectionsService = new CollectionsService({
18
18
  collectionsRepository: collectionsRepositoryInMemory,
@@ -34,8 +34,12 @@ class CollectionsServiceWrapper {
34
34
  const config = require('../../../shared/config');
35
35
  const labs = require('../../../shared/labs');
36
36
 
37
- // host setting OR labs "collections" flag has to be enabled to run collections service
38
- if (config.get('hostSettings:collections:enabled') || labs.isSet('collections')) {
37
+ // CASE: emergency kill switch in case we need to disable collections outside of labs
38
+ if (config.get('hostSettings:collections:enabled') === false) {
39
+ return;
40
+ }
41
+
42
+ if (labs.isSet('collections')) {
39
43
  if (inited) {
40
44
  return;
41
45
  }
@@ -8,12 +8,6 @@ const BLOCK_ACCESS = false;
8
8
 
9
9
  // TODO: better place to store this?
10
10
  const MEMBER_NQL_EXPANSIONS = [{
11
- key: 'labels',
12
- replacement: 'labels.slug'
13
- }, {
14
- key: 'label',
15
- replacement: 'labels.slug'
16
- }, {
17
11
  key: 'products',
18
12
  replacement: 'products.slug'
19
13
  }, {
@@ -21,6 +15,16 @@ const MEMBER_NQL_EXPANSIONS = [{
21
15
  replacement: 'products.slug'
22
16
  }];
23
17
 
18
+ const rejectUnknownKeys = input => nql.utils.mapQuery(input, function (value, key) {
19
+ if (!['product', 'products', 'status'].includes(key.toLowerCase())) {
20
+ return;
21
+ }
22
+
23
+ return {
24
+ [key]: value
25
+ };
26
+ });
27
+
24
28
  /**
25
29
  * @param {object} post - A post object to check access to
26
30
  * @param {object} member - The member whos access should be checked
@@ -50,7 +54,7 @@ function checkPostAccess(post, member) {
50
54
  }).join(',');
51
55
  }
52
56
 
53
- if (visibility && member.status && nql(visibility, {expansions: MEMBER_NQL_EXPANSIONS}).queryJSON(member)) {
57
+ if (visibility && member.status && nql(visibility, {expansions: MEMBER_NQL_EXPANSIONS, transformer: rejectUnknownKeys}).queryJSON(member)) {
54
58
  return PERMIT_ACCESS;
55
59
  }
56
60
 
@@ -34,7 +34,13 @@ module.exports = class WebmentionMetadata {
34
34
  */
35
35
  async fetch(url) {
36
36
  const mappedUrl = this.#getMappedUrl(url);
37
- const data = await oembedService.fetchOembedDataFromUrl(mappedUrl.href, 'mention');
37
+ const data = await oembedService.fetchOembedDataFromUrl(mappedUrl.href, 'mention', {
38
+ timeout: 15000,
39
+ retry: {
40
+ // Only retry on network issues, or specific HTTP status codes
41
+ limit: 3
42
+ }
43
+ });
38
44
 
39
45
  const result = {
40
46
  siteTitle: data.metadata.publisher,
@@ -50,7 +56,13 @@ module.exports = class WebmentionMetadata {
50
56
  if (mappedUrl.href !== url.href) {
51
57
  // Still need to fetch body and contentType separately now
52
58
  // For verification
53
- const {body, contentType} = await oembedService.fetchPageHtml(url);
59
+ const {body, contentType} = await oembedService.fetchPageHtml(url, {
60
+ timeout: 15000,
61
+ retry: {
62
+ // Only retry on network issues, or specific HTTP status codes
63
+ limit: 3
64
+ }
65
+ });
54
66
  result.body = body;
55
67
  result.contentType = contentType;
56
68
  }
@@ -14,7 +14,7 @@ module.exports = function getSiteProperties() {
14
14
  locale: settingsCache.get('locale'),
15
15
  url: urlUtils.urlFor('home', true),
16
16
  version: ghostVersion.safe,
17
- allow_self_signup: settingsCache.get('allow_self_signup')
17
+ allow_external_signup: settingsCache.get('allow_self_signup') && !(settingsCache.get('portal_signup_checkbox_required') && settingsCache.get('portal_signup_terms_html'))
18
18
  };
19
19
 
20
20
  if (config.get('client_sentry') && !config.get('client_sentry').disabled) {
@@ -45,7 +45,7 @@ class RecommendationServiceWrapper {
45
45
 
46
46
  const mentions = require('../mentions');
47
47
 
48
- if (!mentions.sendingService) {
48
+ if (!mentions.sendingService || !mentions.api) {
49
49
  // eslint-disable-next-line ghost/ghost-custom/no-native-error
50
50
  throw new Error('MentionSendingService not intialized, but this is a dependency of RecommendationServiceWrapper. Check boot order.');
51
51
  }
@@ -75,7 +75,8 @@ class RecommendationServiceWrapper {
75
75
  wellknownService,
76
76
  mentionSendingService: mentions.sendingService,
77
77
  clickEventRepository: this.clickEventRepository,
78
- subscribeEventRepository: this.subscribeEventRepository
78
+ subscribeEventRepository: this.subscribeEventRepository,
79
+ mentionsApi: mentions.api
79
80
  });
80
81
  this.controller = new RecommendationController({
81
82
  service: this.service
@@ -46,7 +46,7 @@ module.exports = function apiRoutes() {
46
46
  router.del('/posts/:id', mw.authAdminApi, http(api.posts.destroy));
47
47
  router.post('/posts/:id/copy', mw.authAdminApi, http(api.posts.copy));
48
48
 
49
- router.get('/mentions', labs.enabledMiddleware('webmentions'), mw.authAdminApi, http(api.mentions.browse));
49
+ router.get('/mentions', mw.authAdminApi, http(api.mentions.browse));
50
50
 
51
51
  router.put('/comments/:id', mw.authAdminApi, http(api.comments.edit));
52
52
 
@@ -349,6 +349,7 @@ module.exports = function apiRoutes() {
349
349
 
350
350
  // Recommendations
351
351
  router.get('/recommendations', mw.authAdminApi, http(api.recommendations.browse));
352
+ router.get('/recommendations/:id', mw.authAdminApi, http(api.recommendations.read));
352
353
  router.post('/recommendations', mw.authAdminApi, http(api.recommendations.add));
353
354
  router.put('/recommendations/:id', mw.authAdminApi, http(api.recommendations.edit));
354
355
  router.del('/recommendations/:id', mw.authAdminApi, http(api.recommendations.destroy));
@@ -201,10 +201,6 @@
201
201
  "url": "https://cdn.jsdelivr.net/ghost/koenig-lexical@~{version}/dist/koenig-lexical.umd.js",
202
202
  "version": "0.4"
203
203
  },
204
- "adminX": {
205
- "url": "https://cdn.jsdelivr.net/ghost/admin-x-settings@~{version}/dist/admin-x-settings.js",
206
- "version": "0.0"
207
- },
208
204
  "signupForm": {
209
205
  "url": "https://cdn.jsdelivr.net/ghost/signup-form@~{version}/umd/signup-form.min.js",
210
206
  "version": "0.1"
@@ -15,6 +15,7 @@ const messages = {
15
15
  // flags in this list always return `true`, allows quick global enable prior to full flag removal
16
16
  const GA_FEATURES = [
17
17
  'audienceFeedback',
18
+ 'collections',
18
19
  'themeErrorsNotification',
19
20
  'outboundLinkTagging',
20
21
  'announcementBar',
@@ -36,7 +37,6 @@ const ALPHA_FEATURES = [
36
37
  'websockets',
37
38
  'stripeAutomaticTax',
38
39
  'emailCustomization',
39
- 'collections',
40
40
  'adminXSettings',
41
41
  'mailEvents',
42
42
  'collectionsCard',