ghost 5.9.4 → 5.11.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 (178) hide show
  1. package/components/tryghost-adapter-manager-5.11.0.tgz +0 -0
  2. package/components/tryghost-api-framework-5.11.0.tgz +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-0.0.0.tgz → tryghost-api-version-compatibility-service-5.11.0.tgz} +0 -0
  4. package/components/tryghost-bootstrap-socket-5.11.0.tgz +0 -0
  5. package/components/tryghost-constants-5.11.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-5.11.0.tgz +0 -0
  7. package/components/tryghost-domain-events-5.11.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-5.11.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-service-5.11.0.tgz +0 -0
  10. package/components/tryghost-email-content-generator-5.11.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-5.11.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-5.11.0.tgz +0 -0
  13. package/components/tryghost-html-to-plaintext-5.11.0.tgz +0 -0
  14. package/components/tryghost-job-manager-5.11.0.tgz +0 -0
  15. package/components/tryghost-magic-link-5.11.0.tgz +0 -0
  16. package/components/tryghost-mailgun-client-5.11.0.tgz +0 -0
  17. package/components/tryghost-member-analytics-service-5.11.0.tgz +0 -0
  18. package/components/tryghost-member-attribution-5.11.0.tgz +0 -0
  19. package/components/tryghost-member-events-5.11.0.tgz +0 -0
  20. package/components/tryghost-members-analytics-ingress-5.11.0.tgz +0 -0
  21. package/components/tryghost-members-api-5.11.0.tgz +0 -0
  22. package/components/{tryghost-members-csv-0.0.0.tgz → tryghost-members-csv-5.11.0.tgz} +0 -0
  23. package/components/tryghost-members-events-service-5.11.0.tgz +0 -0
  24. package/components/tryghost-members-importer-5.11.0.tgz +0 -0
  25. package/components/tryghost-members-offers-5.11.0.tgz +0 -0
  26. package/components/tryghost-members-payments-5.11.0.tgz +0 -0
  27. package/components/tryghost-members-ssr-5.11.0.tgz +0 -0
  28. package/components/tryghost-members-stripe-service-5.11.0.tgz +0 -0
  29. package/components/tryghost-minifier-5.11.0.tgz +0 -0
  30. package/components/tryghost-mw-api-version-mismatch-5.11.0.tgz +0 -0
  31. package/components/tryghost-mw-cache-control-5.11.0.tgz +0 -0
  32. package/components/tryghost-mw-error-handler-5.11.0.tgz +0 -0
  33. package/components/tryghost-mw-session-from-token-5.11.0.tgz +0 -0
  34. package/components/tryghost-mw-update-user-last-seen-5.11.0.tgz +0 -0
  35. package/components/tryghost-mw-vhost-5.11.0.tgz +0 -0
  36. package/components/tryghost-oembed-service-5.11.0.tgz +0 -0
  37. package/components/tryghost-package-json-5.11.0.tgz +0 -0
  38. package/components/tryghost-security-5.11.0.tgz +0 -0
  39. package/components/tryghost-session-service-5.11.0.tgz +0 -0
  40. package/components/tryghost-settings-path-manager-5.11.0.tgz +0 -0
  41. package/components/tryghost-update-check-service-5.11.0.tgz +0 -0
  42. package/components/tryghost-verification-trigger-5.11.0.tgz +0 -0
  43. package/components/tryghost-version-notifications-data-service-5.11.0.tgz +0 -0
  44. package/content/themes/casper/assets/built/screen.css +1 -1
  45. package/content/themes/casper/assets/built/screen.css.map +1 -1
  46. package/content/themes/casper/assets/css/screen.css +8 -5
  47. package/content/themes/casper/package.json +1 -1
  48. package/core/boot.js +2 -0
  49. package/core/bridge.js +2 -0
  50. package/core/built/admin/assets/chunk.143.14589cc066b8120b73e3.js +49 -0
  51. package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.ae492405065373dbe102.js} +31 -29
  52. package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.131e85a10d2031148425.js} +4 -4
  53. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.579.65e09dd89eec70d059a0.js} +23 -28
  54. package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.579.65e09dd89eec70d059a0.js.LICENSE.txt} +0 -0
  55. package/core/built/admin/assets/ghost-1b0d7c731511bb738ec457d2932c43c0.css +1 -0
  56. package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-40f5bd12d121c54bbc39e7939e78244f.js} +827 -611
  57. package/core/built/admin/assets/ghost-dark-7b2825a050b0382630180f48aa78ea5d.css +1 -0
  58. package/core/built/admin/assets/icons/calendar-stroke.svg +1 -0
  59. package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
  60. package/core/built/admin/assets/icons/pen-stroke.svg +1 -0
  61. package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
  62. package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-741dc0e4078e044a0c9bfaad104de8b3.js} +85 -78
  63. package/core/built/admin/index.html +6 -6
  64. package/core/frontend/helpers/ghost_head.js +4 -0
  65. package/core/frontend/helpers/search.js +42 -0
  66. package/core/frontend/services/member-attribution-assets/index.js +4 -0
  67. package/core/frontend/services/member-attribution-assets/service.js +83 -0
  68. package/core/frontend/src/member-attribution/.eslintrc +10 -0
  69. package/core/frontend/src/member-attribution/member-attribution.js +90 -0
  70. package/core/frontend/web/site.js +3 -0
  71. package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
  72. package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
  73. package/core/server/api/endpoints/comments-members.js +10 -7
  74. package/core/server/api/endpoints/invites.js +1 -9
  75. package/core/server/api/endpoints/labels.js +1 -7
  76. package/core/server/api/endpoints/members.js +3 -13
  77. package/core/server/api/endpoints/offers.js +2 -2
  78. package/core/server/api/endpoints/pages.js +2 -10
  79. package/core/server/api/endpoints/posts.js +11 -10
  80. package/core/server/api/endpoints/snippets.js +1 -9
  81. package/core/server/api/endpoints/tags.js +1 -7
  82. package/core/server/api/endpoints/utils/serializers/input/pages.js +1 -1
  83. package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
  84. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
  85. package/core/server/api/endpoints/utils/serializers/output/members.js +2 -1
  86. package/core/server/api/endpoints/utils/serializers/output/site.js +1 -0
  87. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +6 -7
  88. package/core/server/api/endpoints/webhooks.js +2 -19
  89. package/core/server/data/exporter/table-lists.js +2 -0
  90. package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
  91. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
  92. package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
  93. package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
  94. package/core/server/data/migrations/versions/5.11/2022-08-22-11-03-add-member-alert-settings-columns-to-users.js +21 -0
  95. package/core/server/data/migrations/versions/5.11/2022-08-23-13-41-backfill-members-created-events.js +32 -0
  96. package/core/server/data/migrations/versions/5.11/2022-08-23-13-59-fix-page-resource-type.js +22 -0
  97. package/core/server/data/schema/fixtures/fixtures.json +3 -0
  98. package/core/server/data/schema/schema.js +24 -2
  99. package/core/server/lib/image/cached-image-size-from-url.js +52 -28
  100. package/core/server/lib/image/gravatar.js +8 -7
  101. package/core/server/lib/image/image-size.js +60 -56
  102. package/core/server/lib/image/image-utils.js +5 -2
  103. package/core/server/lib/image/index.js +14 -1
  104. package/core/server/models/action.js +0 -10
  105. package/core/server/models/api-key.js +3 -18
  106. package/core/server/models/base/plugins/actions.js +55 -0
  107. package/core/server/models/integration.js +3 -0
  108. package/core/server/models/label.js +3 -18
  109. package/core/server/models/member-created-event.js +26 -0
  110. package/core/server/models/member.js +54 -4
  111. package/core/server/models/offer.js +3 -0
  112. package/core/server/models/post.js +25 -18
  113. package/core/server/models/product.js +3 -0
  114. package/core/server/models/settings.js +4 -0
  115. package/core/server/models/subscription-created-event.js +30 -0
  116. package/core/server/models/tag.js +3 -18
  117. package/core/server/models/user.js +7 -19
  118. package/core/server/models/webhook.js +3 -0
  119. package/core/server/services/auth/api-key/admin.js +0 -3
  120. package/core/server/services/auth/passwordreset.js +0 -3
  121. package/core/server/services/comments/emails.js +3 -3
  122. package/core/server/services/explore/service.js +8 -6
  123. package/core/server/services/member-attribution/index.js +52 -0
  124. package/core/server/services/members/api.js +3 -1
  125. package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
  126. package/core/server/services/members/jobs/index.js +27 -0
  127. package/core/server/services/members/service.js +14 -8
  128. package/core/server/services/public-config/site.js +1 -0
  129. package/core/server/services/route-settings/default-settings-manager.js +19 -17
  130. package/core/server/services/settings/settings-service.js +1 -1
  131. package/core/server/services/webhooks/trigger.js +14 -5
  132. package/core/shared/config/defaults.json +8 -3
  133. package/core/shared/labs.js +5 -2
  134. package/package.json +84 -83
  135. package/yarn.lock +440 -615
  136. package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
  137. package/components/tryghost-api-framework-0.0.0.tgz +0 -0
  138. package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
  139. package/components/tryghost-constants-0.0.0.tgz +0 -0
  140. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  141. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  142. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  143. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  144. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  145. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  146. package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
  147. package/components/tryghost-html-to-plaintext-0.0.0.tgz +0 -0
  148. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  149. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  150. package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
  151. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  152. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  153. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  154. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  155. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  156. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  157. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  158. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  159. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  160. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  161. package/components/tryghost-minifier-0.0.0.tgz +0 -0
  162. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  163. package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
  164. package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
  165. package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
  166. package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
  167. package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
  168. package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
  169. package/components/tryghost-package-json-0.0.0.tgz +0 -0
  170. package/components/tryghost-security-0.0.0.tgz +0 -0
  171. package/components/tryghost-session-service-0.0.0.tgz +0 -0
  172. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  173. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  174. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  175. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  176. package/core/built/admin/assets/chunk.143.1c158e8ef19f10e5439c.js +0 -41
  177. package/core/built/admin/assets/ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css +0 -1
  178. package/core/built/admin/assets/ghost-facfdf4a7d9759c5b681340805f21fd8.css +0 -1
@@ -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
+ );
@@ -0,0 +1,21 @@
1
+ const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');
2
+
3
+ module.exports = combineNonTransactionalMigrations(
4
+ createAddColumnMigration('users', 'free_member_signup_notification', {
5
+ type: 'boolean',
6
+ nullable: false,
7
+ defaultTo: true
8
+ }),
9
+
10
+ createAddColumnMigration('users', 'paid_subscription_canceled_notification', {
11
+ type: 'boolean',
12
+ nullable: false,
13
+ defaultTo: false
14
+ }),
15
+
16
+ createAddColumnMigration('users', 'paid_subscription_started_notification', {
17
+ type: 'boolean',
18
+ nullable: false,
19
+ defaultTo: true
20
+ })
21
+ );
@@ -0,0 +1,32 @@
1
+ const ObjectID = require('bson-objectid').default;
2
+ const logging = require('@tryghost/logging');
3
+
4
+ const {createTransactionalMigration} = require('../../utils');
5
+
6
+ module.exports = createTransactionalMigration(
7
+ async function up(knex) {
8
+ const members = await knex('members')
9
+ .select('id', 'created_at');
10
+
11
+ if (members.length === 0) {
12
+ logging.warn(`Skipping migration because no members found`);
13
+ return;
14
+ }
15
+
16
+ const toInsert = members.map((member) => {
17
+ return {
18
+ id: ObjectID().toHexString(),
19
+ member_id: member.id,
20
+ created_at: member.created_at,
21
+ source: 'member'
22
+ };
23
+ });
24
+
25
+ logging.info(`Inserting ${toInsert.length} members created events`);
26
+ await knex.batchInsert('members_created_events', toInsert);
27
+ },
28
+ async function down(knex) {
29
+ logging.info(`Clearing all members created events`);
30
+ await knex('members_created_events').del();
31
+ }
32
+ );
@@ -0,0 +1,22 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ logging.info(`Changing Action event 'page' resource_type to 'post'`);
7
+
8
+ const affectedRows = await knex('actions')
9
+ .update({
10
+ resource_type: 'post',
11
+ context: JSON.stringify({
12
+ type: 'page'
13
+ })
14
+ })
15
+ .where('resource_type', 'page');
16
+
17
+ logging.info(`Updated ${affectedRows} Action events from 'page' to 'post'`);
18
+ },
19
+ async function down() {
20
+ // no-op: we don't want to put `pages` back as a resource type
21
+ }
22
+ );
@@ -626,6 +626,9 @@
626
626
  "name": "Ghost",
627
627
  "email": "ghost@example.com",
628
628
  "status": "inactive",
629
+ "free_member_signup_notification": true,
630
+ "paid_subscription_started_notification": true,
631
+ "paid_subscription_canceled_notification": false,
629
632
  "roles": []
630
633
  }
631
634
  ]
@@ -149,6 +149,9 @@ module.exports = {
149
149
  tour: {type: 'text', maxlength: 65535, nullable: true},
150
150
  last_seen: {type: 'dateTime', nullable: true},
151
151
  comment_notifications: {type: 'boolean', nullable: false, defaultTo: true},
152
+ free_member_signup_notification: {type: 'boolean', nullable: false, defaultTo: true},
153
+ paid_subscription_started_notification: {type: 'boolean', nullable: false, defaultTo: true},
154
+ paid_subscription_canceled_notification: {type: 'boolean', nullable: false, defaultTo: false},
152
155
  created_at: {type: 'dateTime', nullable: false},
153
156
  created_by: {type: 'string', maxlength: 24, nullable: false},
154
157
  updated_at: {type: 'dateTime', nullable: true},
@@ -466,7 +469,8 @@ module.exports = {
466
469
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
467
470
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
468
471
  product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
469
- sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
472
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
473
+ expiry_at: {type: 'dateTime', nullable: true}
470
474
  },
471
475
  posts_products: {
472
476
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -474,6 +478,15 @@ module.exports = {
474
478
  product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
475
479
  sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
476
480
  },
481
+ members_created_events: {
482
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
483
+ created_at: {type: 'dateTime', nullable: false},
484
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
485
+ attribution_id: {type: 'string', maxlength: 24, nullable: true},
486
+ attribution_type: {type: 'string', maxlength: 50, nullable: true},
487
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true},
488
+ source: {type: 'string', maxlength: 50, nullable: false}
489
+ },
477
490
  members_cancel_events: {
478
491
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
479
492
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
@@ -594,6 +607,15 @@ module.exports = {
594
607
  plan_amount: {type: 'integer', nullable: false},
595
608
  plan_currency: {type: 'string', maxLength: 3, nullable: false}
596
609
  },
610
+ members_subscription_created_events: {
611
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
612
+ created_at: {type: 'dateTime', nullable: false},
613
+ member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
614
+ subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
615
+ attribution_id: {type: 'string', maxlength: 24, nullable: true},
616
+ attribution_type: {type: 'string', maxlength: 50, nullable: true},
617
+ attribution_url: {type: 'string', maxlength: 2000, nullable: true}
618
+ },
597
619
  offer_redemptions: {
598
620
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
599
621
  offer_id: {type: 'string', maxlength: 24, nullable: false, references: 'offers.id', cascadeDelete: true},
@@ -754,7 +776,7 @@ module.exports = {
754
776
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
755
777
  post_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'posts.id', cascadeDelete: true},
756
778
  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'},
779
+ parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id', cascadeDelete: true},
758
780
  status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'published', validations: {isIn: [['published', 'hidden', 'deleted']]}},
759
781
  html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
760
782
  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
 
@@ -40,15 +40,16 @@ class Gravatar {
40
40
  image: imageUrl
41
41
  };
42
42
  })
43
- .catch({statusCode: 404}, function () {
44
- return {
45
- image: undefined
46
- };
47
- })
48
- .catch(function () {
43
+ .catch(function (err) {
44
+ if (err.statusCode === 404) {
45
+ return {
46
+ image: undefined
47
+ };
48
+ }
49
+
49
50
  // ignore error, just resolve with no image url
50
51
  });
51
52
  }
52
53
  }
53
54
 
54
- module.exports = Gravatar;
55
+ module.exports = Gravatar;
@@ -163,39 +163,41 @@ class ImageSize {
163
163
  width: dimensions.width,
164
164
  height: dimensions.height
165
165
  };
166
- }).catch({code: 'URL_MISSING_INVALID'}, (err) => {
167
- return Promise.reject(new errors.InternalServerError({
168
- message: err.message,
169
- code: 'IMAGE_SIZE_URL',
170
- statusCode: err.statusCode,
171
- context: err.url || imagePath
172
- }));
173
- }).catch({code: 'ETIMEDOUT'}, {code: 'ESOCKETTIMEDOUT'}, {code: 'ECONNRESET'}, {statusCode: 408}, (err) => {
174
- return Promise.reject(new errors.InternalServerError({
175
- message: 'Request timed out.',
176
- code: 'IMAGE_SIZE_URL',
177
- statusCode: err.statusCode,
178
- context: err.url || imagePath
179
- }));
180
- }).catch({code: 'ENOENT'}, {code: 'ENOTFOUND'}, {statusCode: 404}, (err) => {
181
- return Promise.reject(new errors.NotFoundError({
182
- message: 'Image not found.',
183
- code: 'IMAGE_SIZE_URL',
184
- statusCode: err.statusCode,
185
- context: err.url || imagePath
186
- }));
187
- }).catch(function (err) {
188
- if (errors.utils.isGhostError(err)) {
189
- return Promise.reject(err);
190
- }
166
+ }).catch((err) => {
167
+ if (err.code === 'URL_MISSING_INVALID') {
168
+ return Promise.reject(new errors.InternalServerError({
169
+ message: err.message,
170
+ code: 'IMAGE_SIZE_URL',
171
+ statusCode: err.statusCode,
172
+ context: err.url || imagePath
173
+ }));
174
+ } else if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT' || err.code === 'ECONNRESET' || err.statusCode === 408) {
175
+ return Promise.reject(new errors.InternalServerError({
176
+ message: 'Request timed out.',
177
+ code: 'IMAGE_SIZE_URL',
178
+ statusCode: err.statusCode,
179
+ context: err.url || imagePath
180
+ }));
181
+ } else if (err.code === 'ENOENT' || err.code === 'ENOTFOUND' || err.statusCode === 404) {
182
+ return Promise.reject(new errors.NotFoundError({
183
+ message: 'Image not found.',
184
+ code: 'IMAGE_SIZE_URL',
185
+ statusCode: err.statusCode,
186
+ context: err.url || imagePath
187
+ }));
188
+ } else {
189
+ if (errors.utils.isGhostError(err)) {
190
+ return Promise.reject(err);
191
+ }
191
192
 
192
- return Promise.reject(new errors.InternalServerError({
193
- message: 'Unknown Request error.',
194
- code: 'IMAGE_SIZE_URL',
195
- statusCode: err.statusCode,
196
- context: err.url || imagePath,
197
- err: err
198
- }));
193
+ return Promise.reject(new errors.InternalServerError({
194
+ message: 'Unknown Request error.',
195
+ code: 'IMAGE_SIZE_URL',
196
+ statusCode: err.statusCode,
197
+ context: err.url || imagePath,
198
+ err: err
199
+ }));
200
+ }
199
201
  });
200
202
  }
201
203
 
@@ -237,32 +239,34 @@ class ImageSize {
237
239
  height: dimensions.height
238
240
  };
239
241
  })
240
- .catch({code: 'ENOENT'}, (err) => {
241
- return Promise.reject(new errors.NotFoundError({
242
- message: err.message,
243
- code: 'IMAGE_SIZE_STORAGE',
244
- err: err,
245
- context: filePath,
246
- errorDetails: {
247
- originalPath: imagePath,
248
- reqFilePath: filePath
242
+ .catch((err) => {
243
+ if (err.code === 'ENOENT') {
244
+ return Promise.reject(new errors.NotFoundError({
245
+ message: err.message,
246
+ code: 'IMAGE_SIZE_STORAGE',
247
+ err: err,
248
+ context: filePath,
249
+ errorDetails: {
250
+ originalPath: imagePath,
251
+ reqFilePath: filePath
252
+ }
253
+ }));
254
+ } else {
255
+ if (errors.utils.isGhostError(err)) {
256
+ return Promise.reject(err);
249
257
  }
250
- }));
251
- }).catch((err) => {
252
- if (errors.utils.isGhostError(err)) {
253
- return Promise.reject(err);
254
- }
255
258
 
256
- return Promise.reject(new errors.InternalServerError({
257
- message: err.message,
258
- code: 'IMAGE_SIZE_STORAGE',
259
- err: err,
260
- context: filePath,
261
- errorDetails: {
262
- originalPath: imagePath,
263
- reqFilePath: filePath
264
- }
265
- }));
259
+ return Promise.reject(new errors.InternalServerError({
260
+ message: err.message,
261
+ code: 'IMAGE_SIZE_STORAGE',
262
+ err: err,
263
+ context: filePath,
264
+ errorDetails: {
265
+ originalPath: imagePath,
266
+ reqFilePath: filePath
267
+ }
268
+ }));
269
+ }
266
270
  });
267
271
  }
268
272
 
@@ -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
+ });
@@ -19,16 +19,6 @@ const Action = ghostBookshelf.Model.extend({
19
19
 
20
20
  resource() {
21
21
  return this.morphTo('resource', ['resource_type', 'resource_id'], ...candidates);
22
- },
23
-
24
- toJSON(unfilteredOptions) {
25
- const options = Action.filterOptions(unfilteredOptions, 'toJSON');
26
- const attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
27
-
28
- // @TODO: context is not implemented yet
29
- delete attrs.context;
30
-
31
- return attrs;
32
22
  }
33
23
  }, {
34
24
  orderDefaultOptions: function orderDefaultOptions() {
@@ -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,61 @@ 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
+ if (!resourceType) {
33
+ return;
34
+ }
35
+
36
+ let context = {};
37
+
38
+ if (this.actionsExtraContext && Array.isArray(this.actionsExtraContext)) {
39
+ for (const c of this.actionsExtraContext) {
40
+ context[c] = this.get(c) || this.previous(c);
41
+ }
42
+ }
43
+
44
+ if (event === 'deleted') {
45
+ context.primary_name = (this.previous('title') || this.previous('name'));
46
+ } else if (['added', 'edited'].includes(event)) {
47
+ context.primary_name = (this.get('title') || this.get('name') || this.previous('title') || this.previous('name'));
48
+ }
49
+
50
+ const data = {
51
+ event,
52
+ resource_id: this.id || this.previous('id'),
53
+ resource_type: resourceType,
54
+ actor_id: actor.id,
55
+ actor_type: actor.type
56
+ };
57
+
58
+ if (context && Object.keys(context).length) {
59
+ data.context = context;
60
+ }
61
+
62
+ return data;
63
+ },
64
+
10
65
  /**
11
66
  * @NOTE:
12
67
  *
@@ -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: {