ghost 5.9.4 → 5.10.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 (85) hide show
  1. package/components/tryghost-api-framework-0.0.0.tgz +0 -0
  2. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  3. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  4. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  5. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  6. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  7. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  8. package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
  9. package/components/tryghost-member-attribution-0.0.0.tgz +0 -0
  10. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  11. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  12. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  13. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  14. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  15. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  16. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  17. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  18. package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
  19. package/components/tryghost-security-0.0.0.tgz +0 -0
  20. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  21. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  22. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  23. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  24. package/content/themes/casper/assets/built/screen.css +1 -1
  25. package/content/themes/casper/assets/built/screen.css.map +1 -1
  26. package/content/themes/casper/assets/css/screen.css +8 -5
  27. package/content/themes/casper/package.json +1 -1
  28. package/core/boot.js +2 -0
  29. package/core/bridge.js +2 -0
  30. package/core/built/admin/assets/{chunk.143.1c158e8ef19f10e5439c.js → chunk.143.6a3c46a89c731b86a730.js} +6 -6
  31. package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.0364e8abdae8210d8e6d.js} +31 -29
  32. package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.8a19c35ce1a7cf4249ce.js} +4 -4
  33. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.351.ea4a4ff4b40d5f2ad141.js} +22 -19
  34. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.351.ea4a4ff4b40d5f2ad141.js.LICENSE.txt} +0 -0
  35. package/core/built/admin/assets/{ghost-facfdf4a7d9759c5b681340805f21fd8.css → ghost-13baab17b3f54b21f341fb8f36f83110.css} +1 -1
  36. package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-ced03a7ac75c3148e0ea7d1bf51e39fc.js} +319 -282
  37. package/core/built/admin/assets/{ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css → ghost-dark-b0500577a42e2770994e6aef0e70f182.css} +1 -1
  38. package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
  39. package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
  40. package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-a1ae7a38d5c38fcba5609eed4e37f02a.js} +73 -70
  41. package/core/built/admin/index.html +6 -6
  42. package/core/frontend/helpers/ghost_head.js +4 -0
  43. package/core/frontend/helpers/search.js +42 -0
  44. package/core/frontend/services/member-attribution-assets/index.js +4 -0
  45. package/core/frontend/services/member-attribution-assets/service.js +83 -0
  46. package/core/frontend/src/member-attribution/.eslintrc +10 -0
  47. package/core/frontend/src/member-attribution/member-attribution.js +90 -0
  48. package/core/frontend/web/site.js +3 -0
  49. package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
  50. package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
  51. package/core/server/api/endpoints/posts.js +10 -1
  52. package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
  53. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
  54. package/core/server/data/exporter/table-lists.js +2 -0
  55. package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
  56. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
  57. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
  58. package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
  59. package/core/server/data/schema/schema.js +21 -2
  60. package/core/server/lib/image/cached-image-size-from-url.js +52 -28
  61. package/core/server/lib/image/image-utils.js +5 -2
  62. package/core/server/lib/image/index.js +14 -1
  63. package/core/server/models/api-key.js +3 -18
  64. package/core/server/models/base/plugins/actions.js +32 -0
  65. package/core/server/models/integration.js +3 -0
  66. package/core/server/models/label.js +3 -18
  67. package/core/server/models/member-created-event.js +26 -0
  68. package/core/server/models/member.js +36 -4
  69. package/core/server/models/post.js +26 -18
  70. package/core/server/models/subscription-created-event.js +30 -0
  71. package/core/server/models/tag.js +3 -18
  72. package/core/server/models/user.js +3 -18
  73. package/core/server/models/webhook.js +3 -0
  74. package/core/server/services/comments/emails.js +3 -3
  75. package/core/server/services/explore/service.js +3 -2
  76. package/core/server/services/member-attribution/index.js +24 -0
  77. package/core/server/services/members/api.js +3 -1
  78. package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
  79. package/core/server/services/members/jobs/index.js +27 -0
  80. package/core/server/services/members/service.js +14 -8
  81. package/core/server/services/settings/settings-service.js +1 -1
  82. package/core/shared/config/defaults.json +6 -2
  83. package/core/shared/labs.js +4 -1
  84. package/package.json +6 -5
  85. package/yarn.lock +349 -507
@@ -0,0 +1,4 @@
1
+ const MemberAttributionAssetsService = require('./service');
2
+ const memberAttributionAssets = new MemberAttributionAssetsService();
3
+
4
+ module.exports = memberAttributionAssets;
@@ -0,0 +1,83 @@
1
+ // const debug = require('@tryghost/debug')('comments-counts-assets');
2
+ const Minifier = require('@tryghost/minifier');
3
+ const path = require('path');
4
+ const fs = require('fs').promises;
5
+ const logging = require('@tryghost/logging');
6
+ const config = require('../../../shared/config');
7
+
8
+ class MemberAttributionAssetsService {
9
+ constructor(options = {}) {
10
+ /** @private */
11
+ this.src = options.src || path.join(config.get('paths').assetSrc, 'member-attribution');
12
+ /** @private */
13
+ this.dest = options.dest || config.getContentPath('public');
14
+ /** @private */
15
+ this.minifier = new Minifier({src: this.src, dest: this.dest});
16
+ }
17
+
18
+ /**
19
+ * @private
20
+ */
21
+ generateGlobs() {
22
+ return {
23
+ 'member-attribution.min.js': '*.js'
24
+ };
25
+ }
26
+
27
+ /**
28
+ * @private
29
+ */
30
+ generateReplacements() {
31
+ return {};
32
+ }
33
+
34
+ /**
35
+ * @private
36
+ * @returns {Promise<void>}
37
+ */
38
+ async minify(globs, options) {
39
+ try {
40
+ await this.minifier.minify(globs, options);
41
+ } catch (error) {
42
+ if (error.code === 'EACCES') {
43
+ logging.error('Ghost was not able to write member-attribution asset files due to permissions.');
44
+ return;
45
+ }
46
+
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @private
53
+ * @returns {Promise<void>}
54
+ */
55
+ async clearFiles() {
56
+ const rmFile = async (name) => {
57
+ await fs.unlink(path.join(this.dest, name));
58
+ };
59
+
60
+ const promises = [];
61
+ for (const key of Object.keys(this.generateGlobs())) {
62
+ // @deprecated switch this to use fs.rm when we drop support for Node v12
63
+ promises.push(rmFile(key));
64
+ }
65
+
66
+ // We don't care if removing these files fails as it's valid for them to not exist
67
+ await Promise.allSettled(promises);
68
+ }
69
+
70
+ /**
71
+ * Minify, move into the destination directory, and clear existing asset files.
72
+ *
73
+ * @returns {Promise<void>}
74
+ */
75
+ async load() {
76
+ const globs = this.generateGlobs();
77
+ const replacements = this.generateReplacements();
78
+ await this.clearFiles();
79
+ await this.minify(globs, {replacements});
80
+ }
81
+ }
82
+
83
+ module.exports = MemberAttributionAssetsService;
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../../.eslintrc.js",
3
+ "env": {
4
+ "browser": true,
5
+ "node": false
6
+ },
7
+ "rules": {
8
+ "no-console": "off"
9
+ }
10
+ }
@@ -0,0 +1,90 @@
1
+ // Location where we want to store the history in localStorage
2
+ const STORAGE_KEY = 'ghost-history';
3
+
4
+ // How long before an item should expire (24h)
5
+ const TIMEOUT = 24 * 60 * 60 * 1000;
6
+
7
+ // Maximum amount of urls in the history
8
+ const LIMIT = 15;
9
+
10
+ // History is saved in JSON format, from old to new
11
+ // Time is saved to be able to exclude old items
12
+ // [
13
+ // {
14
+ // "time": 12341234,
15
+ // "path": "/about/"
16
+ // },
17
+ // {
18
+ // "time": 12341235,
19
+ // "path": "/welcome/"
20
+ // }
21
+ // ]
22
+
23
+ (async function () {
24
+ try {
25
+ const storage = window.localStorage;
26
+ const historyString = storage.getItem(STORAGE_KEY);
27
+ const currentTime = new Date().getTime();
28
+
29
+ // Append current location
30
+ let history = [];
31
+
32
+ if (historyString) {
33
+ try {
34
+ history = JSON.parse(historyString);
35
+ } catch (error) {
36
+ // Ignore invalid JSON, ans clear history
37
+ console.warn('[Member Attribution] Error while parsing history', error);
38
+ }
39
+ }
40
+
41
+ // Remove all items that are expired
42
+ const firstNotExpiredIndex = history.findIndex((item) => {
43
+ // Return true to keep all items after and including this item
44
+ // Return false to remove the item
45
+
46
+ if (!item.time || typeof item.time !== 'number') {
47
+ return false;
48
+ }
49
+
50
+ const difference = currentTime - item.time;
51
+
52
+ if (isNaN(item.time) || difference > TIMEOUT) {
53
+ // Expired or invalid
54
+ return false;
55
+ }
56
+
57
+ // Valid item (so all following items are also valid by definition)
58
+ return true;
59
+ });
60
+
61
+ if (firstNotExpiredIndex > 0) {
62
+ // Remove until the first valid item
63
+ history.splice(0, firstNotExpiredIndex);
64
+ } else if (firstNotExpiredIndex === -1) {
65
+ // Not a single valid item found, remove all
66
+ history = [];
67
+ }
68
+
69
+ const currentPath = window.location.pathname;
70
+
71
+ if (history.length === 0 || history[history.length - 1].path !== currentPath) {
72
+ history.push({
73
+ path: currentPath,
74
+ time: currentTime
75
+ });
76
+ } else if (history.length > 0) {
77
+ history[history.length - 1].time = currentTime;
78
+ }
79
+
80
+ // Restrict length
81
+ if (history.length > LIMIT) {
82
+ history = history.slice(-LIMIT);
83
+ }
84
+
85
+ // Save current timestamp
86
+ storage.setItem(STORAGE_KEY, JSON.stringify(history));
87
+ } catch (error) {
88
+ console.error('[Member Attribution] Failed with error', error);
89
+ }
90
+ })();
@@ -75,6 +75,9 @@ module.exports = function setupSiteApp(routerConfig) {
75
75
  // Comment counts
76
76
  siteApp.use(mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', constants.ONE_YEAR_S));
77
77
 
78
+ // Member attribution
79
+ siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', constants.ONE_YEAR_S));
80
+
78
81
  // Serve blog images using the storage adapter
79
82
  siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
80
83
  // Serve blog media using the storage adapter
@@ -0,0 +1,7 @@
1
+ const Memory = require('./Memory');
2
+
3
+ class ImageSizesCacheSyncInMemory extends Memory {
4
+
5
+ }
6
+
7
+ module.exports = ImageSizesCacheSyncInMemory;
@@ -0,0 +1,7 @@
1
+ const Memory = require('./Memory');
2
+
3
+ class SettingsCacheSyncInMemory extends Memory {
4
+
5
+ }
6
+
7
+ module.exports = SettingsCacheSyncInMemory;
@@ -2,7 +2,16 @@ const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const getPostServiceInstance = require('../../services/posts/posts-service');
5
- const allowedIncludes = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter'];
5
+ const allowedIncludes = [
6
+ 'tags',
7
+ 'authors',
8
+ 'authors.roles',
9
+ 'email',
10
+ 'tiers',
11
+ 'newsletter',
12
+ 'count.signups',
13
+ 'count.conversions'
14
+ ];
6
15
  const unsafeAttrs = ['status', 'authors', 'visibility'];
7
16
 
8
17
  const messages = {
@@ -23,7 +23,7 @@ function defaultRelations(frame) {
23
23
  return false;
24
24
  }
25
25
 
26
- frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter'];
26
+ frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions'];
27
27
  }
28
28
 
29
29
  function setDefaultOrder(frame) {
@@ -10,6 +10,7 @@ const extraAttrs = require('../utils/extra-attrs');
10
10
  const gating = require('../utils/post-gating');
11
11
  const url = require('../utils/url');
12
12
 
13
+ const labs = require('../../../../../../../shared/labs');
13
14
  const utils = require('../../../index');
14
15
 
15
16
  const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;
@@ -109,5 +110,9 @@ module.exports = async (model, frame, options = {}) => {
109
110
  });
110
111
  }
111
112
 
113
+ if (!labs.isSet('memberAttribution')) {
114
+ delete jsonModel.count;
115
+ }
116
+
112
117
  return jsonModel;
113
118
  };
@@ -31,6 +31,8 @@ const BACKUP_TABLES = [
31
31
  'members_paid_subscription_events',
32
32
  'members_subscribe_events',
33
33
  'members_product_events',
34
+ 'members_created_events',
35
+ 'members_subscription_created_events',
34
36
  'members_newsletters',
35
37
  'comments',
36
38
  'comment_likes',
@@ -0,0 +1,6 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('members_products', 'expiry_at', {
4
+ type: 'dateTime',
5
+ nullable: true
6
+ });
@@ -0,0 +1,11 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('members_created_events', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ created_at: {type: 'dateTime', nullable: false},
6
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
7
+ attribution_id: {type: 'string', maxlength: 24, nullable: true},
8
+ attribution_type: {type: 'string', maxlength: 50, nullable: true},
9
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true},
10
+ source: {type: 'string', maxlength: 50, nullable: false}
11
+ });
@@ -0,0 +1,11 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('members_subscription_created_events', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ created_at: {type: 'dateTime', nullable: false},
6
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
7
+ subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
8
+ attribution_id: {type: 'string', maxlength: 24, nullable: true},
9
+ attribution_type: {type: 'string', maxlength: 50, nullable: true},
10
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true}
11
+ });
@@ -0,0 +1,45 @@
1
+ const {addForeign, dropForeign} = require('../../../schema/commands');
2
+ const logging = require('@tryghost/logging');
3
+ const {createTransactionalMigration} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Adding on delete CASCADE for comments parent_id');
8
+
9
+ await dropForeign({
10
+ fromTable: 'comments',
11
+ fromColumn: 'parent_id',
12
+ toTable: 'comments',
13
+ toColumn: 'id',
14
+ transaction: knex
15
+ });
16
+
17
+ await addForeign({
18
+ fromTable: 'comments',
19
+ fromColumn: 'parent_id',
20
+ toTable: 'comments',
21
+ toColumn: 'id',
22
+ cascadeDelete: true,
23
+ transaction: knex
24
+ });
25
+ },
26
+ async function down(knex) {
27
+ logging.info('Restoring foreign key for comments parent_id');
28
+
29
+ await dropForeign({
30
+ fromTable: 'comments',
31
+ fromColumn: 'parent_id',
32
+ toTable: 'comments',
33
+ toColumn: 'id',
34
+ transaction: knex
35
+ });
36
+
37
+ await addForeign({
38
+ fromTable: 'comments',
39
+ fromColumn: 'parent_id',
40
+ toTable: 'comments',
41
+ toColumn: 'id',
42
+ transaction: knex
43
+ });
44
+ }
45
+ );
@@ -466,7 +466,8 @@ module.exports = {
466
466
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
467
467
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
468
468
  product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
469
- sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
469
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
470
+ expiry_at: {type: 'dateTime', nullable: true}
470
471
  },
471
472
  posts_products: {
472
473
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -474,6 +475,15 @@ module.exports = {
474
475
  product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
475
476
  sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
476
477
  },
478
+ members_created_events: {
479
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
480
+ created_at: {type: 'dateTime', nullable: false},
481
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
482
+ attribution_id: {type: 'string', maxlength: 24, nullable: true},
483
+ attribution_type: {type: 'string', maxlength: 50, nullable: true},
484
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true},
485
+ source: {type: 'string', maxlength: 50, nullable: false}
486
+ },
477
487
  members_cancel_events: {
478
488
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
479
489
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
@@ -594,6 +604,15 @@ module.exports = {
594
604
  plan_amount: {type: 'integer', nullable: false},
595
605
  plan_currency: {type: 'string', maxLength: 3, nullable: false}
596
606
  },
607
+ members_subscription_created_events: {
608
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
609
+ created_at: {type: 'dateTime', nullable: false},
610
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
611
+ subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
612
+ attribution_id: {type: 'string', maxlength: 24, nullable: true},
613
+ attribution_type: {type: 'string', maxlength: 50, nullable: true},
614
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true}
615
+ },
597
616
  offer_redemptions: {
598
617
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
599
618
  offer_id: {type: 'string', maxlength: 24, nullable: false, references: 'offers.id', cascadeDelete: true},
@@ -754,7 +773,7 @@ module.exports = {
754
773
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
755
774
  post_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'posts.id', cascadeDelete: true},
756
775
  member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id', setNullDelete: true},
757
- parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id'},
776
+ parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id', cascadeDelete: true},
758
777
  status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'published', validations: {isIn: [['published', 'hidden', 'deleted']]}},
759
778
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
760
779
  edited_at: {type: 'dateTime', nullable: true},
@@ -1,52 +1,76 @@
1
1
  const debug = require('@tryghost/debug')('utils:image-size-cache');
2
2
  const errors = require('@tryghost/errors');
3
3
  const logging = require('@tryghost/logging');
4
+
5
+ /**
6
+ * @example
7
+ * {
8
+ * height: 50,
9
+ * url: 'https://mysite.com/images/cat.jpg',
10
+ * width: 50
11
+ * }
12
+ * @typedef ImageSizeCache
13
+ * @type {Object}
14
+ * @property {string} url image url
15
+ * @property {number} height image height
16
+ * @property {number} width image width
17
+ */
18
+
4
19
  class CachedImageSizeFromUrl {
5
- constructor({imageSize}) {
6
- this.imageSize = imageSize;
7
- this.cache = new Map();
20
+ /**
21
+ *
22
+ * @param {Object} options
23
+ * @param {(url: string) => Promise<ImageSizeCache>} options.getImageSizeFromUrl - method that resolves images based on URL
24
+ * @param {Object} options.cache - cache store instance
25
+ */
26
+ constructor({getImageSizeFromUrl, cache}) {
27
+ this.getImageSizeFromUrl = getImageSizeFromUrl;
28
+ this.cache = cache;
8
29
  }
9
30
 
10
31
  /**
11
32
  * Get cached image size from URL
12
33
  * Always returns {object} imageSizeCache
13
34
  * @param {string} url
14
- * @returns {Promise<Object>} imageSizeCache
35
+ * @returns {Promise<ImageSizeCache>}
15
36
  * @description Takes a url and returns image width and height from cache if available.
16
37
  * If not in cache, `getImageSizeFromUrl` is called and returns the dimensions in a Promise.
17
38
  */
18
- getCachedImageSizeFromUrl(url) {
39
+ async getCachedImageSizeFromUrl(url) {
19
40
  if (!url || url === undefined || url === null) {
20
41
  return;
21
42
  }
22
-
23
- // image size is not in cache
24
- if (!this.cache.has(url)) {
25
- return this.imageSize.getImageSizeFromUrl(url).then((res) => {
26
- this.cache.set(url, res);
27
-
43
+
44
+ const cachedImageSize = await this.cache.get(url);
45
+
46
+ if (cachedImageSize) {
47
+ debug('Read image from cache:', url);
48
+
49
+ return cachedImageSize;
50
+ } else {
51
+ try {
52
+ const res = await this.getImageSizeFromUrl(url);
53
+ await this.cache.set(url, res);
54
+
28
55
  debug('Cached image:', url);
29
-
30
- return this.cache.get(url);
31
- }).catch(errors.NotFoundError, () => {
32
- debug('Cached image (not found):', url);
33
- // in case of error we just attach the url
34
- this.cache.set(url, url);
35
-
56
+
36
57
  return this.cache.get(url);
37
- }).catch((err) => {
38
- debug('Cached image (error):', url);
39
- logging.error(err);
40
-
58
+ } catch (err) {
59
+ if (err instanceof errors.NotFoundError) {
60
+ debug('Cached image (not found):', url);
61
+ } else {
62
+ debug('Cached image (error):', url);
63
+ logging.error(err);
64
+ }
65
+
41
66
  // in case of error we just attach the url
42
- this.cache.set(url, url);
43
-
67
+ await this.cache.set(url, {
68
+ url
69
+ });
70
+
44
71
  return this.cache.get(url);
45
- });
72
+ }
46
73
  }
47
- debug('Read image from cache:', url);
48
- // returns image size from cache
49
- return this.cache.get(url);
50
74
  }
51
75
  }
52
76
 
@@ -4,10 +4,13 @@ const Gravatar = require('./gravatar');
4
4
  const ImageSize = require('./image-size');
5
5
 
6
6
  class ImageUtils {
7
- constructor({config, urlUtils, settingsCache, storageUtils, storage, validator, request}) {
7
+ constructor({config, urlUtils, settingsCache, storageUtils, storage, validator, request, cacheStore}) {
8
8
  this.blogIcon = new BlogIcon({config, urlUtils, settingsCache, storageUtils});
9
9
  this.imageSize = new ImageSize({config, storage, storageUtils, validator, urlUtils, request});
10
- this.cachedImageSizeFromUrl = new CachedImageSizeFromUrl({imageSize: this.imageSize});
10
+ this.cachedImageSizeFromUrl = new CachedImageSizeFromUrl({
11
+ getImageSizeFromUrl: this.imageSize.getImageSizeFromUrl.bind(this.imageSize),
12
+ cache: cacheStore
13
+ });
11
14
  this.gravatar = new Gravatar({config, request});
12
15
  }
13
16
  }
@@ -7,4 +7,17 @@ const config = require('../../../shared/config');
7
7
  const settingsCache = require('../../../shared/settings-cache');
8
8
  const ImageUtils = require('./image-utils');
9
9
 
10
- module.exports = new ImageUtils({config, urlUtils, settingsCache, storageUtils, storage, validator, request});
10
+ const adapterManager = require('../../services/adapter-manager');
11
+
12
+ const cacheStore = adapterManager.getAdapter('cache:imageSizes');
13
+
14
+ module.exports = new ImageUtils({
15
+ config,
16
+ urlUtils,
17
+ settingsCache,
18
+ storageUtils,
19
+ storage,
20
+ validator,
21
+ request,
22
+ cacheStore
23
+ });
@@ -6,6 +6,9 @@ const {Role} = require('./role');
6
6
  const ApiKey = ghostBookshelf.Model.extend({
7
7
  tableName: 'api_keys',
8
8
 
9
+ actionsCollectCRUD: true,
10
+ actionsResourceType: 'api_key',
11
+
9
12
  defaults() {
10
13
  const secret = security.secret.create(this.get('type'));
11
14
 
@@ -53,24 +56,6 @@ const ApiKey = ghostBookshelf.Model.extend({
53
56
  if (this.previous('secret') !== this.get('secret')) {
54
57
  this.addAction(model, 'refreshed', options);
55
58
  }
56
- },
57
-
58
- getAction(event, options) {
59
- const actor = this.getActor(options);
60
-
61
- // @NOTE: we ignore internal updates (`options.context.internal`) for now
62
- if (!actor) {
63
- return;
64
- }
65
-
66
- // @TODO: implement context
67
- return {
68
- event: event,
69
- resource_id: this.id || this.previous('id'),
70
- resource_type: 'api_key',
71
- actor_id: actor.id,
72
- actor_type: actor.type
73
- };
74
59
  }
75
60
  }, {
76
61
  refreshSecret(data, options) {
@@ -7,6 +7,38 @@ const logging = require('@tryghost/logging');
7
7
  */
8
8
  module.exports = function (Bookshelf) {
9
9
  Bookshelf.Model = Bookshelf.Model.extend({
10
+ /**
11
+ * Constructs data to be stored in the database with info
12
+ * on particular actions
13
+ */
14
+ getAction(event, options) {
15
+ const actor = this.getActor(options);
16
+
17
+ // @NOTE: we ignore internal updates (`options.context.internal`) for now
18
+ if (!actor) {
19
+ return;
20
+ }
21
+
22
+ if (!this.actionsCollectCRUD) {
23
+ return;
24
+ }
25
+
26
+ let resourceType = this.actionsResourceType;
27
+
28
+ if (typeof resourceType === 'function') {
29
+ resourceType = resourceType.bind(this)();
30
+ }
31
+
32
+ // @TODO: implement context
33
+ return {
34
+ event: event,
35
+ resource_id: this.id || this.previous('id'),
36
+ resource_type: resourceType,
37
+ actor_id: actor.id,
38
+ actor_type: actor.type
39
+ };
40
+ },
41
+
10
42
  /**
11
43
  * @NOTE:
12
44
  *
@@ -6,6 +6,9 @@ const {NoPermissionError} = require('@tryghost/errors');
6
6
  const Integration = ghostBookshelf.Model.extend({
7
7
  tableName: 'integrations',
8
8
 
9
+ actionsCollectCRUD: true,
10
+ actionsResourceType: 'integration',
11
+
9
12
  relationships: ['api_keys', 'webhooks'],
10
13
 
11
14
  relationshipBelongsTo: {