ghost 5.33.8 → 5.34.1

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 (165) hide show
  1. package/components/tryghost-adapter-cache-redis-5.34.1.tgz +0 -0
  2. package/components/tryghost-adapter-manager-5.34.1.tgz +0 -0
  3. package/components/{tryghost-api-framework-5.33.8.tgz → tryghost-api-framework-5.34.1.tgz} +0 -0
  4. package/components/tryghost-api-version-compatibility-service-5.34.1.tgz +0 -0
  5. package/components/tryghost-audience-feedback-5.34.1.tgz +0 -0
  6. package/components/tryghost-bootstrap-socket-5.34.1.tgz +0 -0
  7. package/components/tryghost-constants-5.34.1.tgz +0 -0
  8. package/components/{tryghost-custom-theme-settings-service-5.33.8.tgz → tryghost-custom-theme-settings-service-5.34.1.tgz} +0 -0
  9. package/components/tryghost-data-generator-5.34.1.tgz +0 -0
  10. package/components/tryghost-domain-events-5.34.1.tgz +0 -0
  11. package/components/tryghost-dynamic-routing-events-5.34.1.tgz +0 -0
  12. package/components/tryghost-email-analytics-provider-mailgun-5.34.1.tgz +0 -0
  13. package/components/tryghost-email-analytics-service-5.34.1.tgz +0 -0
  14. package/components/tryghost-email-content-generator-5.34.1.tgz +0 -0
  15. package/components/tryghost-email-events-5.34.1.tgz +0 -0
  16. package/components/tryghost-email-service-5.34.1.tgz +0 -0
  17. package/components/tryghost-email-suppression-list-5.34.1.tgz +0 -0
  18. package/components/{tryghost-express-dynamic-redirects-5.33.8.tgz → tryghost-express-dynamic-redirects-5.34.1.tgz} +0 -0
  19. package/components/tryghost-extract-api-key-5.34.1.tgz +0 -0
  20. package/components/tryghost-html-to-plaintext-5.34.1.tgz +0 -0
  21. package/components/tryghost-i18n-5.34.1.tgz +0 -0
  22. package/components/{tryghost-importer-revue-5.33.8.tgz → tryghost-importer-revue-5.34.1.tgz} +0 -0
  23. package/components/{tryghost-job-manager-5.33.8.tgz → tryghost-job-manager-5.34.1.tgz} +0 -0
  24. package/components/tryghost-link-redirects-5.34.1.tgz +0 -0
  25. package/components/tryghost-link-replacer-5.34.1.tgz +0 -0
  26. package/components/tryghost-link-tracking-5.34.1.tgz +0 -0
  27. package/components/{tryghost-magic-link-5.33.8.tgz → tryghost-magic-link-5.34.1.tgz} +0 -0
  28. package/components/tryghost-mailgun-client-5.34.1.tgz +0 -0
  29. package/components/{tryghost-member-attribution-5.33.8.tgz → tryghost-member-attribution-5.34.1.tgz} +0 -0
  30. package/components/tryghost-member-events-5.34.1.tgz +0 -0
  31. package/components/tryghost-members-api-5.34.1.tgz +0 -0
  32. package/components/tryghost-members-csv-5.34.1.tgz +0 -0
  33. package/components/{tryghost-members-events-service-5.33.8.tgz → tryghost-members-events-service-5.34.1.tgz} +0 -0
  34. package/components/{tryghost-members-importer-5.33.8.tgz → tryghost-members-importer-5.34.1.tgz} +0 -0
  35. package/components/tryghost-members-offers-5.34.1.tgz +0 -0
  36. package/components/tryghost-members-payments-5.34.1.tgz +0 -0
  37. package/components/tryghost-members-ssr-5.34.1.tgz +0 -0
  38. package/components/tryghost-members-stripe-service-5.34.1.tgz +0 -0
  39. package/components/tryghost-milestone-emails-5.34.1.tgz +0 -0
  40. package/components/tryghost-minifier-5.34.1.tgz +0 -0
  41. package/components/tryghost-mw-api-version-mismatch-5.34.1.tgz +0 -0
  42. package/components/{tryghost-mw-cache-control-5.33.8.tgz → tryghost-mw-cache-control-5.34.1.tgz} +0 -0
  43. package/components/{tryghost-mw-error-handler-5.33.8.tgz → tryghost-mw-error-handler-5.34.1.tgz} +0 -0
  44. package/components/tryghost-mw-session-from-token-5.34.1.tgz +0 -0
  45. package/components/tryghost-mw-update-user-last-seen-5.34.1.tgz +0 -0
  46. package/components/tryghost-mw-vhost-5.34.1.tgz +0 -0
  47. package/components/tryghost-oembed-service-5.34.1.tgz +0 -0
  48. package/components/{tryghost-package-json-5.33.8.tgz → tryghost-package-json-5.34.1.tgz} +0 -0
  49. package/components/tryghost-referrers-5.34.1.tgz +0 -0
  50. package/components/tryghost-security-5.34.1.tgz +0 -0
  51. package/components/tryghost-session-service-5.34.1.tgz +0 -0
  52. package/components/tryghost-settings-path-manager-5.34.1.tgz +0 -0
  53. package/components/tryghost-staff-service-5.34.1.tgz +0 -0
  54. package/components/tryghost-stats-service-5.34.1.tgz +0 -0
  55. package/components/tryghost-tags-public-5.34.1.tgz +0 -0
  56. package/components/tryghost-tiers-5.34.1.tgz +0 -0
  57. package/components/{tryghost-update-check-service-5.33.8.tgz → tryghost-update-check-service-5.34.1.tgz} +0 -0
  58. package/components/tryghost-verification-trigger-5.34.1.tgz +0 -0
  59. package/components/tryghost-version-notifications-data-service-5.34.1.tgz +0 -0
  60. package/components/tryghost-webmentions-5.34.1.tgz +0 -0
  61. package/core/boot.js +6 -1
  62. package/core/built/admin/assets/{chunk.143.caa7dbd727b76021c31b.js → chunk.143.07f5af56ff872bb0e9e4.js} +14 -14
  63. package/core/built/admin/assets/{chunk.178.9803e6194593a0ed6595.js → chunk.178.2e831ef9072743e38dd1.js} +4 -4
  64. package/core/built/admin/assets/{chunk.963.e47ead5abeca4cf69fed.js → chunk.616.181e1ad6c33f0bec7a65.js} +3519 -3512
  65. package/core/built/admin/assets/{chunk.963.e47ead5abeca4cf69fed.js.LICENSE.txt → chunk.616.181e1ad6c33f0bec7a65.js.LICENSE.txt} +0 -0
  66. package/core/built/admin/assets/{chunk.79.c3c2c05ea7ff7707fcad.js → chunk.79.ec143a398298020c87e6.js} +112 -112
  67. package/core/built/admin/assets/codemirror/{codemirror-a81c0653d8e57286b75c5a1792f80779.js → codemirror-6c43f4894cbd8db73d7f35cde836c58e.js} +810 -809
  68. package/core/built/admin/assets/{ghost-fb9fb8adbcaf1603ad4006dd2d49e401.css → ghost-558c1e319d6e025bfab2054bc0f7fe83.css} +1 -1
  69. package/core/built/admin/assets/{ghost-872d240189f9b5ff85f4f7cb2ac3ab4c.js → ghost-ad40d109653288e74a7cd922341fb33d.js} +114 -89
  70. package/core/built/admin/assets/{ghost-dark-ac0cb221eddc8652a0e7c263ed6513dc.css → ghost-dark-a15754df1f9070dc2525482ce22e2251.css} +1 -1
  71. package/core/built/admin/assets/simplemde/{simplemde-2885e4a40ed66fcae974595584efe50b.js → simplemde-28049a9bd7f432b0648747eb26958a33.js} +836 -836
  72. package/core/built/admin/assets/{vendor-0441964c34d58f2aacd5a04bbe240243.js → vendor-253d6527ca6353855164ef65f896f762.js} +1208 -1233
  73. package/core/built/admin/index.html +6 -6
  74. package/core/cli/generate-data.js +21 -3
  75. package/core/frontend/services/routing/router-manager.js +1 -1
  76. package/core/frontend/web/middleware/handle-image-sizes.js +6 -21
  77. package/core/server/adapters/cache/Redis.js +3 -0
  78. package/core/server/adapters/storage/LocalStorageBase.js +11 -1
  79. package/core/server/api/endpoints/images.js +47 -6
  80. package/core/server/api/endpoints/mentions.js +1 -1
  81. package/core/server/api/endpoints/tags-public.js +2 -1
  82. package/core/server/api/endpoints/utils/serializers/output/mappers/mentions.js +1 -1
  83. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +1 -0
  84. package/core/server/data/migrations/versions/5.34/2023-01-30-07-27-add-mentions-permission.js +10 -0
  85. package/core/server/data/migrations/versions/5.34/2023-02-08-03-08-add-mentions-notifications-column.js +7 -0
  86. package/core/server/data/migrations/versions/5.34/2023-02-08-22-32-add-mentions-delete-column.js +7 -0
  87. package/core/server/data/schema/fixtures/fixtures.json +9 -2
  88. package/core/server/data/schema/schema.js +3 -1
  89. package/core/server/lib/image/image-size.js +23 -5
  90. package/core/server/lib/lexical.js +36 -3
  91. package/core/server/models/member.js +3 -0
  92. package/core/server/models/mention.js +7 -1
  93. package/core/server/models/post.js +1 -1
  94. package/core/server/models/user.js +4 -1
  95. package/core/server/services/adapter-manager/index.js +7 -0
  96. package/core/server/services/email-analytics/wrapper.js +22 -13
  97. package/core/server/services/email-service/wrapper.js +1 -1
  98. package/core/server/services/mega/post-email-serializer.js +1 -1
  99. package/core/server/services/members/jobs/index.js +4 -2
  100. package/core/server/services/mentions/BookshelfMentionRepository.js +3 -2
  101. package/core/server/services/mentions/MentionController.js +78 -12
  102. package/core/server/services/mentions/service.js +43 -3
  103. package/core/server/services/milestone-emails/MilestoneQueries.js +58 -0
  104. package/core/server/services/milestone-emails/index.js +1 -0
  105. package/core/server/services/milestone-emails/service.js +58 -0
  106. package/core/server/services/tags-public/index.js +1 -0
  107. package/core/server/services/tags-public/service.js +31 -0
  108. package/core/server/web/api/endpoints/admin/routes.js +0 -1
  109. package/core/server/web/api/middleware/index.js +0 -1
  110. package/core/server/web/shared/middleware/api/spam-prevention.js +31 -1
  111. package/core/server/web/shared/middleware/brute.js +13 -0
  112. package/core/server/web/webmentions/routes.js +3 -0
  113. package/core/shared/config/defaults.json +17 -1
  114. package/core/shared/config/env/config.development.json +0 -3
  115. package/core/shared/config/env/config.testing-browser.json +6 -0
  116. package/core/shared/config/env/config.testing-mysql.json +7 -0
  117. package/core/shared/config/env/config.testing.json +6 -0
  118. package/core/shared/labs.js +3 -3
  119. package/package.json +121 -114
  120. package/yarn.lock +327 -242
  121. package/components/tryghost-adapter-manager-5.33.8.tgz +0 -0
  122. package/components/tryghost-api-version-compatibility-service-5.33.8.tgz +0 -0
  123. package/components/tryghost-audience-feedback-5.33.8.tgz +0 -0
  124. package/components/tryghost-bootstrap-socket-5.33.8.tgz +0 -0
  125. package/components/tryghost-constants-5.33.8.tgz +0 -0
  126. package/components/tryghost-data-generator-5.33.8.tgz +0 -0
  127. package/components/tryghost-domain-events-5.33.8.tgz +0 -0
  128. package/components/tryghost-dynamic-routing-events-5.33.8.tgz +0 -0
  129. package/components/tryghost-email-analytics-provider-mailgun-5.33.8.tgz +0 -0
  130. package/components/tryghost-email-analytics-service-5.33.8.tgz +0 -0
  131. package/components/tryghost-email-content-generator-5.33.8.tgz +0 -0
  132. package/components/tryghost-email-events-5.33.8.tgz +0 -0
  133. package/components/tryghost-email-service-5.33.8.tgz +0 -0
  134. package/components/tryghost-email-suppression-list-5.33.8.tgz +0 -0
  135. package/components/tryghost-extract-api-key-5.33.8.tgz +0 -0
  136. package/components/tryghost-html-to-plaintext-5.33.8.tgz +0 -0
  137. package/components/tryghost-i18n-5.33.8.tgz +0 -0
  138. package/components/tryghost-link-redirects-5.33.8.tgz +0 -0
  139. package/components/tryghost-link-replacer-5.33.8.tgz +0 -0
  140. package/components/tryghost-link-tracking-5.33.8.tgz +0 -0
  141. package/components/tryghost-mailgun-client-5.33.8.tgz +0 -0
  142. package/components/tryghost-member-events-5.33.8.tgz +0 -0
  143. package/components/tryghost-members-api-5.33.8.tgz +0 -0
  144. package/components/tryghost-members-csv-5.33.8.tgz +0 -0
  145. package/components/tryghost-members-offers-5.33.8.tgz +0 -0
  146. package/components/tryghost-members-payments-5.33.8.tgz +0 -0
  147. package/components/tryghost-members-ssr-5.33.8.tgz +0 -0
  148. package/components/tryghost-members-stripe-service-5.33.8.tgz +0 -0
  149. package/components/tryghost-minifier-5.33.8.tgz +0 -0
  150. package/components/tryghost-mw-api-version-mismatch-5.33.8.tgz +0 -0
  151. package/components/tryghost-mw-session-from-token-5.33.8.tgz +0 -0
  152. package/components/tryghost-mw-update-user-last-seen-5.33.8.tgz +0 -0
  153. package/components/tryghost-mw-vhost-5.33.8.tgz +0 -0
  154. package/components/tryghost-oembed-service-5.33.8.tgz +0 -0
  155. package/components/tryghost-referrers-5.33.8.tgz +0 -0
  156. package/components/tryghost-security-5.33.8.tgz +0 -0
  157. package/components/tryghost-session-service-5.33.8.tgz +0 -0
  158. package/components/tryghost-settings-path-manager-5.33.8.tgz +0 -0
  159. package/components/tryghost-staff-service-5.33.8.tgz +0 -0
  160. package/components/tryghost-stats-service-5.33.8.tgz +0 -0
  161. package/components/tryghost-tiers-5.33.8.tgz +0 -0
  162. package/components/tryghost-verification-trigger-5.33.8.tgz +0 -0
  163. package/components/tryghost-version-notifications-data-service-5.33.8.tgz +0 -0
  164. package/components/tryghost-webmentions-5.33.8.tgz +0 -0
  165. package/core/server/web/api/middleware/normalize-image.js +0 -42
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.33%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.34%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-3e6947aa681f0fb82b193090e520dc73.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-fb9fb8adbcaf1603ad4006dd2d49e401.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-558c1e319d6e025bfab2054bc0f7fe83.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-0441964c34d58f2aacd5a04bbe240243.js"></script>
60
- <script src="assets/chunk.963.e47ead5abeca4cf69fed.js"></script>
61
- <script src="assets/chunk.143.caa7dbd727b76021c31b.js"></script>
62
- <script src="assets/ghost-872d240189f9b5ff85f4f7cb2ac3ab4c.js"></script>
59
+ <script src="assets/vendor-253d6527ca6353855164ef65f896f762.js"></script>
60
+ <script src="assets/chunk.616.181e1ad6c33f0bec7a65.js"></script>
61
+ <script src="assets/chunk.143.07f5af56ff872bb0e9e4.js"></script>
62
+ <script src="assets/ghost-ad40d109653288e74a7cd922341fb33d.js"></script>
63
63
  </body>
64
64
  </html>
@@ -2,10 +2,13 @@ const Command = require('./command');
2
2
  const DataGenerator = require('@tryghost/data-generator');
3
3
  const config = require('../shared/config');
4
4
 
5
- module.exports = class REPL extends Command {
5
+ module.exports = class DataGeneratorCommand extends Command {
6
6
  setup() {
7
7
  this.help('Generates random data to populate the database for development & testing');
8
8
  this.argument('--base-data-pack', {type: 'string', defaultValue: '', desc: 'Base data pack file location, imported instead of random content'});
9
+ this.argument('--scale', {type: 'string', defaultValue: 'small', desc: 'Scale of the data to generate. `small` for a quick run, `large` for more content'});
10
+ this.argument('--single-table', {type: 'string', desc: 'Import a single table'});
11
+ this.argument('--quantity', {type: 'number', desc: 'When importing a single table, the quantity to import'});
9
12
  }
10
13
 
11
14
  initializeContext(context) {
@@ -27,6 +30,17 @@ module.exports = class REPL extends Command {
27
30
  async handle(argv = {}) {
28
31
  const knex = require('../server/data/db/connection');
29
32
  const {tables: schema} = require('../server/data/schema/index');
33
+
34
+ const modelQuantities = {};
35
+ if (argv.scale) {
36
+ if (argv.scale === 'small') {
37
+ modelQuantities.members = 200;
38
+ modelQuantities.membersLoginEvents = 1;
39
+ modelQuantities.posts = 10;
40
+ }
41
+ // Defaults in data-generator package make a large set
42
+ }
43
+
30
44
  const dataGenerator = new DataGenerator({
31
45
  baseDataPack: argv['base-data-pack'],
32
46
  knex,
@@ -40,11 +54,15 @@ module.exports = class REPL extends Command {
40
54
  fatal: this.fatal,
41
55
  debug: this.debug
42
56
  },
43
- modelQuantities: {},
57
+ modelQuantities,
44
58
  baseUrl: config.getSiteUrl()
45
59
  });
46
60
  try {
47
- await dataGenerator.importData();
61
+ if (argv['single-table']) {
62
+ await dataGenerator.importSingleTable(argv['single-table'], argv.quantity ?? undefined);
63
+ } else {
64
+ await dataGenerator.importData();
65
+ }
48
66
  } catch (error) {
49
67
  this.fatal('Failed while generating data: ', error);
50
68
  }
@@ -177,7 +177,7 @@ class RouterManager {
177
177
 
178
178
  // NOTE: timezone change only affects the collection router with dated permalinks
179
179
  const collectionRouter = this.registry.getRouterByName('CollectionRouter');
180
- if (collectionRouter.getPermalinks().getValue().match(/:year|:month|:day/)) {
180
+ if (collectionRouter && collectionRouter.getPermalinks().getValue().match(/:year|:month|:day/)) {
181
181
  debug('handleTimezoneEdit: trigger regeneration');
182
182
 
183
183
  this.urlService.onRouterUpdated(collectionRouter.identifier);
@@ -5,6 +5,7 @@ const imageTransform = require('@tryghost/image-transform');
5
5
  const storage = require('../../../server/adapters/storage');
6
6
  const activeTheme = require('../../services/theme-engine/active');
7
7
  const config = require('../../../shared/config');
8
+ const {imageSize} = require('../../../server/lib/image');
8
9
 
9
10
  const SIZE_PATH_REGEX = /^\/size\/([^/]+)\//;
10
11
  const FORMAT_PATH_REGEX = /^\/format\/([^./]+)\//;
@@ -21,7 +22,7 @@ module.exports = function (req, res, next) {
21
22
  }
22
23
 
23
24
  const requestedDimension = req.url.match(SIZE_PATH_REGEX)[1];
24
-
25
+
25
26
  // Note that we don't use sizeImageDir because we need to keep the trailing slash
26
27
  let imagePath = req.url.replace(`/size/${requestedDimension}`, '');
27
28
 
@@ -39,7 +40,7 @@ module.exports = function (req, res, next) {
39
40
  // We need to keep the first slash here
40
41
  let url = req.originalUrl
41
42
  .replace(`/size/${requestedDimension}`, '');
42
-
43
+
43
44
  if (format) {
44
45
  url = url.replace(`/format/${format}`, '');
45
46
  }
@@ -81,7 +82,7 @@ module.exports = function (req, res, next) {
81
82
  return redirectToOriginal();
82
83
  }
83
84
 
84
- if (format) {
85
+ if (format) {
85
86
  // CASE: When formatting, we need to check if the imageTransform package supports this specific format
86
87
  if (!imageTransform.canTransformToFormat(format)) {
87
88
  // transform not supported
@@ -89,7 +90,7 @@ module.exports = function (req, res, next) {
89
90
  }
90
91
  }
91
92
 
92
- // CASE: when transforming is supported, we need to check if it is desired
93
+ // CASE: when transforming is supported, we need to check if it is desired
93
94
  // (e.g. it is not desired to resize SVGs when not formatting them to a different type)
94
95
  if (!format && !imageTransform.shouldResizeFileExtension(requestUrlFileExtension)) {
95
96
  return redirectToOriginal();
@@ -111,23 +112,7 @@ module.exports = function (req, res, next) {
111
112
  return;
112
113
  }
113
114
 
114
- const {dir, name, ext} = path.parse(imagePath);
115
- const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
116
-
117
- if (!imageNameMatched) {
118
- // CASE: Image name does not contain any characters?
119
- // RESULT: Hand off to `next()` which will 404
120
- return;
121
- }
122
- const unoptimizedImagePath = path.join(dir, `${imageName}_o${imageNumber || ''}${ext}`);
123
-
124
- return storageInstance.exists(unoptimizedImagePath)
125
- .then((unoptimizedImageExists) => {
126
- if (unoptimizedImageExists) {
127
- return unoptimizedImagePath;
128
- }
129
- return imagePath;
130
- })
115
+ return imageSize.getOriginalImagePath(imagePath)
131
116
  .then((storagePath) => {
132
117
  return storageInstance.read({path: storagePath});
133
118
  })
@@ -0,0 +1,3 @@
1
+ const RedisCache = require('@tryghost/adapter-cache-redis');
2
+
3
+ module.exports = RedisCache;
@@ -83,9 +83,19 @@ class LocalStorageBase extends StorageBase {
83
83
  urlToPath(url) {
84
84
  let filePath;
85
85
 
86
- if (url.match(this.staticFileUrl)) {
86
+ const prefix = urlUtils.urlJoin('/',
87
+ urlUtils.getSubdir(),
88
+ this.staticFileURLPrefix
89
+ );
90
+
91
+ if (url.startsWith(this.staticFileUrl)) {
92
+ // CASE: full path that includes the site url
87
93
  filePath = url.replace(this.staticFileUrl, '');
88
94
  filePath = path.join(this.storagePath, filePath);
95
+ } else if (url.startsWith(prefix)) {
96
+ // CASE: The result of the save method doesn't include the site url. So we need to handle this case.
97
+ filePath = url.replace(prefix, '');
98
+ filePath = path.join(this.storagePath, filePath);
89
99
  } else {
90
100
  throw new errors.IncorrectUsageError({
91
101
  message: tpl(messages.invalidUrlParameter, {url})
@@ -1,19 +1,60 @@
1
- const Promise = require('bluebird');
1
+ /* eslint-disable ghost/ghost-custom/max-api-complexity */
2
2
  const storage = require('../../adapters/storage');
3
+ const imageTransform = require('@tryghost/image-transform');
4
+ const config = require('../../../shared/config');
5
+ const path = require('path');
3
6
 
4
7
  module.exports = {
5
8
  docName: 'images',
6
9
  upload: {
7
10
  statusCode: 201,
8
11
  permissions: false,
9
- query(frame) {
12
+ async query(frame) {
10
13
  const store = storage.getStorage('images');
11
14
 
12
- if (frame.files) {
13
- return Promise
14
- .map(frame.files, file => store.save(file))
15
- .then(paths => paths[0]);
15
+ // Normalize
16
+ const imageOptimizationOptions = config.get('imageOptimization');
17
+
18
+ // Trim _o from file name (not allowed suffix)
19
+ frame.file.name = frame.file.name.replace(/_o(\.\w+?)$/, '$1');
20
+
21
+ // CASE: image transform is not capable of transforming file (e.g. .gif)
22
+ if (imageTransform.shouldResizeFileExtension(frame.file.ext) && imageOptimizationOptions.resize) {
23
+ const out = `${frame.file.path}_processed`;
24
+ const originalPath = frame.file.path;
25
+
26
+ const options = Object.assign({
27
+ in: originalPath,
28
+ out,
29
+ ext: frame.file.ext,
30
+ width: config.get('imageOptimization:defaultMaxWidth')
31
+ }, imageOptimizationOptions);
32
+
33
+ await imageTransform.resizeFromPath(options);
34
+
35
+ // Store the processed/optimized image
36
+ const processedImageUrl = await store.save({
37
+ ...frame.file,
38
+ path: out
39
+ });
40
+ const processedImagePath = store.urlToPath(processedImageUrl);
41
+
42
+ // Get the path and name of the processed image
43
+ // We want to store the original image on the same name + _o
44
+ // So we need to wait for the first store to finish before generating the name of the original image
45
+ const processedImageName = path.basename(processedImagePath);
46
+ const processedImageDir = path.dirname(processedImagePath);
47
+
48
+ // Store the original image
49
+ await store.save({
50
+ ...frame.file,
51
+ path: originalPath,
52
+ name: imageTransform.generateOriginalImageName(processedImageName)
53
+ }, processedImageDir);
54
+
55
+ return processedImageUrl;
16
56
  }
57
+
17
58
  return store.save(frame.file);
18
59
  }
19
60
  }
@@ -11,7 +11,7 @@ module.exports = {
11
11
  'page',
12
12
  'debug'
13
13
  ],
14
- permissions: false,
14
+ permissions: true,
15
15
  query(frame) {
16
16
  return mentions.controller.browse(frame);
17
17
  }
@@ -2,6 +2,7 @@ const Promise = require('bluebird');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const models = require('../../models');
5
+ const tagsPublicService = require('../../services/tags-public');
5
6
 
6
7
  const ALLOWED_INCLUDES = ['count.posts'];
7
8
 
@@ -31,7 +32,7 @@ module.exports = {
31
32
  },
32
33
  permissions: true,
33
34
  query(frame) {
34
- return models.TagPublic.findPage(frame.options);
35
+ return tagsPublicService.api.browse(frame.options);
35
36
  }
36
37
  },
37
38
 
@@ -7,7 +7,7 @@ module.exports = (model) => {
7
7
  target: json.target,
8
8
  timestamp: json.timestamp,
9
9
  payload: json.payload,
10
- resource_id: json.resourceId,
10
+ resource: json.resource,
11
11
  source_title: json.sourceTitle,
12
12
  source_site_title: json.sourceSiteTitle,
13
13
  source_excerpt: json.sourceExcerpt,
@@ -35,6 +35,7 @@ const author = (attrs, frame) => {
35
35
  delete attrs.free_member_signup_notification;
36
36
  delete attrs.paid_subscription_started_notification;
37
37
  delete attrs.paid_subscription_canceled_notification;
38
+ delete attrs.mention_notifications;
38
39
 
39
40
  // @NOTE: used for night shift
40
41
  delete attrs.accessibility;
@@ -0,0 +1,10 @@
1
+ const {addPermissionWithRoles} = require('../../utils');
2
+
3
+ module.exports = addPermissionWithRoles({
4
+ name: 'Browse mentions',
5
+ action: 'browse',
6
+ object: 'mention'
7
+ }, [
8
+ 'Administrator',
9
+ 'Admin Integration'
10
+ ]);
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('users', 'mention_notifications', {
4
+ type: 'boolean',
5
+ nullable: false,
6
+ defaultTo: true
7
+ });
@@ -0,0 +1,7 @@
1
+ const {createAddColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createAddColumnMigration('mentions', 'deleted', {
4
+ type: 'boolean',
5
+ nullable: false,
6
+ defaultTo: false
7
+ });
@@ -633,6 +633,11 @@
633
633
  "name": "Edit links",
634
634
  "action_type": "edit",
635
635
  "object_type": "link"
636
+ },
637
+ {
638
+ "name": "Browse mentions",
639
+ "action_type": "browse",
640
+ "object_type": "mention"
636
641
  }
637
642
  ]
638
643
  },
@@ -763,7 +768,8 @@
763
768
  "newsletter": "all",
764
769
  "explore": "read",
765
770
  "comment": "all",
766
- "link": "all"
771
+ "link": "all",
772
+ "mention": "browse"
767
773
  },
768
774
  "DB Backup Integration": {
769
775
  "db": "all"
@@ -798,7 +804,8 @@
798
804
  "newsletter": ["browse", "read", "add", "edit"],
799
805
  "explore": "read",
800
806
  "comment": "all",
801
- "link": "all"
807
+ "link": "all",
808
+ "mention": "browse"
802
809
  },
803
810
  "Editor": {
804
811
  "notification": "all",
@@ -154,6 +154,7 @@ module.exports = {
154
154
  free_member_signup_notification: {type: 'boolean', nullable: false, defaultTo: true},
155
155
  paid_subscription_started_notification: {type: 'boolean', nullable: false, defaultTo: true},
156
156
  paid_subscription_canceled_notification: {type: 'boolean', nullable: false, defaultTo: false},
157
+ mention_notifications: {type: 'boolean', nullable: false, defaultTo: true},
157
158
  created_at: {type: 'dateTime', nullable: false},
158
159
  created_by: {type: 'string', maxlength: 24, nullable: false},
159
160
  updated_at: {type: 'dateTime', nullable: true},
@@ -993,6 +994,7 @@ module.exports = {
993
994
  resource_id: {type: 'string', maxlength: 24, nullable: true},
994
995
  resource_type: {type: 'string', maxlength: 50, nullable: true},
995
996
  created_at: {type: 'dateTime', nullable: false},
996
- payload: {type: 'text', maxlength: 65535, nullable: true}
997
+ payload: {type: 'text', maxlength: 65535, nullable: true},
998
+ deleted: {type: 'boolean', nullable: false, defaultTo: false}
997
999
  }
998
1000
  };
@@ -270,18 +270,36 @@ class ImageSize {
270
270
  });
271
271
  }
272
272
 
273
- async getOriginalImageSizeFromStoragePath(imagePath) {
273
+ /**
274
+ * Returns the path of the original image for a given image path (we always store the original image in a separate file, suffixed with _o, while we store a resized version of the image on the original name)
275
+ * TODO: Preferrably we want to move this to a separate image utils package. Currently not really a good place to put it in image lib.
276
+ */
277
+ async getOriginalImagePath(imagePath) {
274
278
  const {dir, name, ext} = path.parse(imagePath);
279
+ const storageInstance = this.storage.getStorage('images');
280
+
281
+ const preferredUnoptimizedImagePath = path.join(dir, `${name}_o${ext}`);
282
+ const preferredUnoptimizedImagePathExists = await storageInstance.exists(preferredUnoptimizedImagePath);
283
+ if (preferredUnoptimizedImagePathExists) {
284
+ return preferredUnoptimizedImagePath;
285
+ }
286
+
287
+ // Legacy format did some magic with the numbers that could cause bugs. We still need to support it for old files.
288
+ // refs https://github.com/TryGhost/Team/issues/481
275
289
  const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
276
290
 
277
291
  if (!imageNameMatched) {
278
- return this.getImageSizeFromStoragePath(imagePath);
292
+ return imagePath;
279
293
  }
280
294
 
281
- const originalImagePath = path.join(dir, `${imageName}_o${imageNumber || ''}${ext}`);
282
- const originalImageExists = await this.storage.getStorage('images').exists(originalImagePath);
295
+ const legacyOriginalImagePath = path.join(dir, `${imageName}_o${imageNumber || ''}${ext}`);
296
+ const legacyOriginalImageExists = await storageInstance.exists(legacyOriginalImagePath);
283
297
 
284
- return this.getImageSizeFromStoragePath(originalImageExists ? originalImagePath : imagePath);
298
+ return legacyOriginalImageExists ? legacyOriginalImagePath : imagePath;
299
+ }
300
+
301
+ async getOriginalImageSizeFromStoragePath(imagePath) {
302
+ return this.getImageSizeFromStoragePath(await this.getOriginalImagePath(imagePath));
285
303
  }
286
304
 
287
305
  _getPathFromUrl(imageUrl) {
@@ -1,23 +1,56 @@
1
+ const path = require('path');
1
2
  const urlUtils = require('../../shared/url-utils');
3
+ const config = require('../../shared/config');
4
+ const storage = require('../adapters/storage');
2
5
 
3
6
  let nodes;
4
7
  let lexicalHtmlRenderer;
5
8
  let urlTransformMap;
6
9
 
10
+ function populateNodes() {
11
+ const {DEFAULT_NODES} = require('@tryghost/kg-default-nodes');
12
+ nodes = DEFAULT_NODES;
13
+ }
14
+
7
15
  module.exports = {
8
16
  get lexicalHtmlRenderer() {
9
17
  if (!lexicalHtmlRenderer) {
18
+ if (!nodes) {
19
+ populateNodes();
20
+ }
21
+
10
22
  const LexicalHtmlRenderer = require('@tryghost/kg-lexical-html-renderer');
11
- lexicalHtmlRenderer = new LexicalHtmlRenderer();
23
+ lexicalHtmlRenderer = new LexicalHtmlRenderer({nodes});
12
24
  }
13
25
 
14
26
  return lexicalHtmlRenderer;
15
27
  },
16
28
 
29
+ render(lexical, userOptions = {}) {
30
+ const options = Object.assign({
31
+ siteUrl: config.get('url'),
32
+ imageOptimization: config.get('imageOptimization'),
33
+ canTransformImage(storagePath) {
34
+ const imageTransform = require('@tryghost/image-transform');
35
+ const {ext} = path.parse(storagePath);
36
+
37
+ // NOTE: the "saveRaw" check is smelly
38
+ return imageTransform.canTransformFiles()
39
+ && imageTransform.shouldResizeFileExtension(ext)
40
+ && typeof storage.getStorage('images').saveRaw === 'function';
41
+ },
42
+ createDocument() {
43
+ const {JSDOM} = require('jsdom');
44
+ return (new JSDOM()).window.document;
45
+ }
46
+ }, userOptions);
47
+
48
+ return this.lexicalHtmlRenderer.render(lexical, options);
49
+ },
50
+
17
51
  get nodes() {
18
52
  if (!nodes) {
19
- const {DEFAULT_NODES} = require('@tryghost/kg-default-nodes');
20
- nodes = DEFAULT_NODES;
53
+ populateNodes();
21
54
  }
22
55
 
23
56
  return nodes;
@@ -37,6 +37,9 @@ const Member = ghostBookshelf.Model.extend({
37
37
  key: 'tiers',
38
38
  replacement: 'products.slug'
39
39
  }, {
40
+ key: 'tier_id',
41
+ replacement: 'products.id'
42
+ },{
40
43
  key: 'newsletters',
41
44
  replacement: 'newsletters.slug'
42
45
  }, {
@@ -1,7 +1,13 @@
1
1
  const ghostBookshelf = require('./base');
2
2
 
3
3
  const Mention = ghostBookshelf.Model.extend({
4
- tableName: 'mentions'
4
+ tableName: 'mentions',
5
+ defaults: {
6
+ deleted: false
7
+ },
8
+ enforcedFilters() {
9
+ return 'deleted:false';
10
+ }
5
11
  });
6
12
 
7
13
  module.exports = {
@@ -681,7 +681,7 @@ Post = ghostBookshelf.Model.extend({
681
681
  )
682
682
  ) {
683
683
  try {
684
- this.set('html', lexicalLib.lexicalHtmlRenderer.render(this.get('lexical')));
684
+ this.set('html', lexicalLib.render(this.get('lexical')));
685
685
  } catch (err) {
686
686
  throw new errors.ValidationError({
687
687
  message: tpl(messages.invalidLexicalStructure),
@@ -68,7 +68,8 @@ User = ghostBookshelf.Model.extend({
68
68
  comment_notifications: true,
69
69
  free_member_signup_notification: true,
70
70
  paid_subscription_started_notification: true,
71
- paid_subscription_canceled_notification: false
71
+ paid_subscription_canceled_notification: false,
72
+ mention_notifications: true
72
73
  };
73
74
  },
74
75
 
@@ -504,6 +505,8 @@ User = ghostBookshelf.Model.extend({
504
505
  filter += '+paid_subscription_started_notification:true';
505
506
  } else if (type === 'paid-canceled') {
506
507
  filter += '+paid_subscription_canceled_notification:true';
508
+ } else if (type === 'mention-received') {
509
+ filter += '+mention_notifications:true';
507
510
  }
508
511
  const updatedOptions = _.merge({}, options, {filter, withRelated: ['roles']});
509
512
  return this.findAll(updatedOptions).then((users) => {
@@ -29,5 +29,12 @@ module.exports = {
29
29
  const {adapterClassName, adapterConfig} = resolveAdapterOptions(name, adapterServiceConfig);
30
30
 
31
31
  return adapterManager.getAdapter(name, adapterClassName, adapterConfig);
32
+ },
33
+
34
+ /**
35
+ * Force recreation of all instances instead of reusing cached instances. Use when editing config file during tests.
36
+ */
37
+ clearCache() {
38
+ adapterManager.clearInstanceCache();
32
39
  }
33
40
  };
@@ -55,6 +55,24 @@ class EmailAnalyticsServiceWrapper {
55
55
  });
56
56
  }
57
57
 
58
+ async fetchLatest() {
59
+ const fetchStartDate = new Date();
60
+ debug('Starting email analytics fetch of latest events');
61
+ const eventStats = await this.service.fetchLatest();
62
+ const fetchEndDate = new Date();
63
+ debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms`);
64
+
65
+ const aggregateStartDate = new Date();
66
+ debug(`Starting email analytics aggregation for ${eventStats.emailIds.length} emails`);
67
+ await this.service.aggregateStats(eventStats);
68
+ const aggregateEndDate = new Date();
69
+ debug(`Finished aggregating email analytics in ${aggregateEndDate.getTime() - aggregateStartDate.getTime()}ms`);
70
+
71
+ logging.info(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate.getTime() - fetchStartDate.getTime()}ms`);
72
+
73
+ return eventStats;
74
+ }
75
+
58
76
  async startFetch() {
59
77
  if (this.fetching) {
60
78
  logging.info('Email analytics fetch already running, skipping');
@@ -64,24 +82,15 @@ class EmailAnalyticsServiceWrapper {
64
82
 
65
83
  logging.info('Email analytics fetch started');
66
84
  try {
67
- const fetchStartDate = new Date();
68
- debug('Starting email analytics fetch of latest events');
69
- const eventStats = await this.service.fetchLatest();
70
- const fetchEndDate = new Date();
71
- debug(`Finished fetching ${eventStats.totalEvents} analytics events in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms`);
72
-
73
- const aggregateStartDate = new Date();
74
- debug(`Starting email analytics aggregation for ${eventStats.emailIds.length} emails`);
75
- await this.service.aggregateStats(eventStats);
76
- const aggregateEndDate = new Date();
77
- debug(`Finished aggregating email analytics in ${aggregateEndDate.getTime() - aggregateStartDate.getTime()}ms`);
78
-
79
- logging.info(`Fetched ${eventStats.totalEvents} events and aggregated stats for ${eventStats.emailIds.length} emails in ${aggregateEndDate.getTime() - fetchStartDate.getTime()}ms`);
85
+ const eventStats = await this.fetchLatest();
80
86
 
81
87
  this.fetching = false;
82
88
  return eventStats;
83
89
  } catch (e) {
84
90
  logging.error(e, 'Error while fetching email analytics');
91
+
92
+ // Log again only the error, otherwise we lose the stack trace
93
+ logging.error(e);
85
94
  }
86
95
  this.fetching = false;
87
96
  }
@@ -58,7 +58,7 @@ class EmailServiceWrapper {
58
58
  settingsHelpers,
59
59
  renderers: {
60
60
  mobiledoc: mobiledocLib.mobiledocHtmlRenderer,
61
- lexical: lexicalLib.lexicalHtmlRenderer
61
+ lexical: lexicalLib
62
62
  },
63
63
  imageSize,
64
64
  urlUtils,
@@ -303,7 +303,7 @@ const PostEmailSerializer = {
303
303
  }
304
304
 
305
305
  if (post.lexical) {
306
- post.html = lexicalLib.lexicalHtmlRenderer.render(
306
+ post.html = lexicalLib.render(
307
307
  post.lexical, {target: 'email', postUrl: post.url}
308
308
  );
309
309
  } else {
@@ -14,10 +14,12 @@ module.exports = {
14
14
  ) {
15
15
  // use a random seconds value to avoid spikes to external APIs on the minute
16
16
  const s = Math.floor(Math.random() * 60); // 0-59
17
+ const m = Math.floor(Math.random() * 60); // 0-59
18
+ const h = Math.floor(Math.random() * 6); // 0-5
17
19
 
18
- // Run everyday at 12:05:X AM to clean all expired complimentary subscriptions
20
+ // Run everyday at {0-5}:XX:XX AM to clean all expired complimentary subscriptions
19
21
  jobsService.addJob({
20
- at: `${s} 5 0 * * *`,
22
+ at: `${s} ${m} ${h} * * *`,
21
23
  job: path.resolve(__dirname, 'clean-expired-comped.js'),
22
24
  name: 'clean-expired-comped'
23
25
  });