ghost 5.80.2 → 5.80.3

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 (175) hide show
  1. package/components/{tryghost-adapter-cache-memory-ttl-5.80.2.tgz → tryghost-adapter-cache-memory-ttl-5.80.3.tgz} +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.80.2.tgz → tryghost-adapter-cache-redis-5.80.3.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.80.2.tgz → tryghost-adapter-manager-5.80.3.tgz} +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.80.3.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.80.2.tgz → tryghost-api-framework-5.80.3.tgz} +0 -0
  6. package/components/{tryghost-api-version-compatibility-service-5.80.2.tgz → tryghost-api-version-compatibility-service-5.80.3.tgz} +0 -0
  7. package/components/tryghost-audience-feedback-5.80.3.tgz +0 -0
  8. package/components/tryghost-bookshelf-repository-5.80.3.tgz +0 -0
  9. package/components/{tryghost-bootstrap-socket-5.80.2.tgz → tryghost-bootstrap-socket-5.80.3.tgz} +0 -0
  10. package/components/tryghost-collections-5.80.3.tgz +0 -0
  11. package/components/{tryghost-constants-5.80.2.tgz → tryghost-constants-5.80.3.tgz} +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.80.3.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.80.2.tgz → tryghost-data-generator-5.80.3.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.80.3.tgz +0 -0
  15. package/components/{tryghost-donations-5.80.2.tgz → tryghost-donations-5.80.3.tgz} +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.80.3.tgz +0 -0
  17. package/components/tryghost-email-addresses-5.80.3.tgz +0 -0
  18. package/components/tryghost-email-analytics-provider-mailgun-5.80.3.tgz +0 -0
  19. package/components/tryghost-email-analytics-service-5.80.3.tgz +0 -0
  20. package/components/tryghost-email-content-generator-5.80.3.tgz +0 -0
  21. package/components/{tryghost-email-events-5.80.2.tgz → tryghost-email-events-5.80.3.tgz} +0 -0
  22. package/components/tryghost-email-service-5.80.3.tgz +0 -0
  23. package/components/tryghost-email-suppression-list-5.80.3.tgz +0 -0
  24. package/components/{tryghost-express-dynamic-redirects-5.80.2.tgz → tryghost-express-dynamic-redirects-5.80.3.tgz} +0 -0
  25. package/components/tryghost-external-media-inliner-5.80.3.tgz +0 -0
  26. package/components/tryghost-extract-api-key-5.80.3.tgz +0 -0
  27. package/components/tryghost-ghost-5.80.3.tgz +0 -0
  28. package/components/{tryghost-html-to-plaintext-5.80.2.tgz → tryghost-html-to-plaintext-5.80.3.tgz} +0 -0
  29. package/components/tryghost-i18n-5.80.3.tgz +0 -0
  30. package/components/tryghost-importer-handler-content-files-5.80.3.tgz +0 -0
  31. package/components/tryghost-importer-revue-5.80.3.tgz +0 -0
  32. package/components/tryghost-in-memory-repository-5.80.3.tgz +0 -0
  33. package/components/tryghost-job-manager-5.80.3.tgz +0 -0
  34. package/components/tryghost-link-redirects-5.80.3.tgz +0 -0
  35. package/components/tryghost-link-replacer-5.80.3.tgz +0 -0
  36. package/components/tryghost-link-tracking-5.80.3.tgz +0 -0
  37. package/components/tryghost-magic-link-5.80.3.tgz +0 -0
  38. package/components/tryghost-mail-events-5.80.3.tgz +0 -0
  39. package/components/tryghost-mailgun-client-5.80.3.tgz +0 -0
  40. package/components/{tryghost-member-attribution-5.80.2.tgz → tryghost-member-attribution-5.80.3.tgz} +0 -0
  41. package/components/{tryghost-member-events-5.80.2.tgz → tryghost-member-events-5.80.3.tgz} +0 -0
  42. package/components/tryghost-members-api-5.80.3.tgz +0 -0
  43. package/components/{tryghost-members-csv-5.80.2.tgz → tryghost-members-csv-5.80.3.tgz} +0 -0
  44. package/components/{tryghost-members-events-service-5.80.2.tgz → tryghost-members-events-service-5.80.3.tgz} +0 -0
  45. package/components/tryghost-members-importer-5.80.3.tgz +0 -0
  46. package/components/{tryghost-members-offers-5.80.2.tgz → tryghost-members-offers-5.80.3.tgz} +0 -0
  47. package/components/tryghost-members-payments-5.80.3.tgz +0 -0
  48. package/components/tryghost-members-ssr-5.80.3.tgz +0 -0
  49. package/components/{tryghost-members-stripe-service-5.80.2.tgz → tryghost-members-stripe-service-5.80.3.tgz} +0 -0
  50. package/components/tryghost-mentions-email-report-5.80.3.tgz +0 -0
  51. package/components/tryghost-milestones-5.80.3.tgz +0 -0
  52. package/components/tryghost-minifier-5.80.3.tgz +0 -0
  53. package/components/tryghost-model-to-domain-event-interceptor-5.80.3.tgz +0 -0
  54. package/components/tryghost-mw-api-version-mismatch-5.80.3.tgz +0 -0
  55. package/components/tryghost-mw-cache-control-5.80.3.tgz +0 -0
  56. package/components/{tryghost-mw-error-handler-5.80.2.tgz → tryghost-mw-error-handler-5.80.3.tgz} +0 -0
  57. package/components/{tryghost-mw-session-from-token-5.80.2.tgz → tryghost-mw-session-from-token-5.80.3.tgz} +0 -0
  58. package/components/{tryghost-mw-update-user-last-seen-5.80.2.tgz → tryghost-mw-update-user-last-seen-5.80.3.tgz} +0 -0
  59. package/components/tryghost-mw-version-match-5.80.3.tgz +0 -0
  60. package/components/tryghost-mw-vhost-5.80.3.tgz +0 -0
  61. package/components/tryghost-nql-filter-expansions-5.80.3.tgz +0 -0
  62. package/components/tryghost-oembed-service-5.80.3.tgz +0 -0
  63. package/components/tryghost-package-json-5.80.3.tgz +0 -0
  64. package/components/{tryghost-post-events-5.80.2.tgz → tryghost-post-events-5.80.3.tgz} +0 -0
  65. package/components/{tryghost-post-revisions-5.80.2.tgz → tryghost-post-revisions-5.80.3.tgz} +0 -0
  66. package/components/tryghost-posts-service-5.80.3.tgz +0 -0
  67. package/components/tryghost-recommendations-5.80.3.tgz +0 -0
  68. package/components/tryghost-referrers-5.80.3.tgz +0 -0
  69. package/components/{tryghost-security-5.80.2.tgz → tryghost-security-5.80.3.tgz} +0 -0
  70. package/components/tryghost-session-service-5.80.3.tgz +0 -0
  71. package/components/tryghost-settings-path-manager-5.80.3.tgz +0 -0
  72. package/components/tryghost-slack-notifications-5.80.3.tgz +0 -0
  73. package/components/{tryghost-staff-service-5.80.2.tgz → tryghost-staff-service-5.80.3.tgz} +0 -0
  74. package/components/{tryghost-stats-service-5.80.2.tgz → tryghost-stats-service-5.80.3.tgz} +0 -0
  75. package/components/tryghost-tiers-5.80.3.tgz +0 -0
  76. package/components/{tryghost-update-check-service-5.80.2.tgz → tryghost-update-check-service-5.80.3.tgz} +0 -0
  77. package/components/tryghost-verification-trigger-5.80.3.tgz +0 -0
  78. package/components/{tryghost-version-notifications-data-service-5.80.2.tgz → tryghost-version-notifications-data-service-5.80.3.tgz} +0 -0
  79. package/components/tryghost-webmentions-5.80.3.tgz +0 -0
  80. package/content/themes/casper/assets/built/screen.css +1 -1
  81. package/content/themes/casper/assets/built/screen.css.map +1 -1
  82. package/content/themes/casper/assets/css/screen.css +4 -0
  83. package/content/themes/casper/default.hbs +3 -3
  84. package/content/themes/casper/package.json +1 -1
  85. package/content/themes/casper/partials/post-card.hbs +1 -1
  86. package/content/themes/casper/post.hbs +3 -3
  87. package/content/themes/source/assets/built/screen.css +1 -1
  88. package/content/themes/source/assets/built/screen.css.map +1 -1
  89. package/content/themes/source/assets/css/screen.css +8 -0
  90. package/content/themes/source/package.json +1 -1
  91. package/core/boot.js +27 -0
  92. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  93. package/core/built/admin/assets/admin-x-demo/{index-0722d3aa.mjs → index-771f4964.mjs} +2 -2
  94. package/core/built/admin/assets/admin-x-demo/{modals-969dbbd3.mjs → modals-4c390700.mjs} +46 -46
  95. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-78d4e20d.mjs → CodeEditorView-00610c2f.mjs} +2 -2
  96. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  97. package/core/built/admin/assets/admin-x-settings/{index-d4d158b7.mjs → index-2deb7a1f.mjs} +4273 -4908
  98. package/core/built/admin/assets/admin-x-settings/{index-8d67ef5d.mjs → index-e308aac8.mjs} +1845 -1814
  99. package/core/built/admin/assets/admin-x-settings/{modals-ae924d1e.mjs → modals-e2ca767c.mjs} +13378 -10736
  100. package/core/built/admin/assets/{chunk.524.c3472d95b4bd1a45a18d.js → chunk.524.c6ed93ff44a9cb49235c.js} +6 -6
  101. package/core/built/admin/assets/{chunk.582.c3f2b4d45f0b280c944c.js → chunk.582.4e31037f1eb4c88f017f.js} +4 -4
  102. package/core/built/admin/assets/{chunk.763.bd01d249eecdcce3856c.js → chunk.763.7849440ec6494d2449e9.js} +426 -624
  103. package/core/built/admin/assets/{ghost-ea7a51891dfca9b7a04aacfffe7dea4e.js → ghost-594a7dd26bdb4fec210b285cab5555cb.js} +14 -14
  104. package/core/built/admin/assets/{ghost-ab71ce6991cc9bdcd6b915a9c984fa27.css → ghost-71cfcdf9410af9b3aec11a28f9a4130f.css} +1 -1
  105. package/core/built/admin/assets/{ghost-dark-fd04889e3b6ac569d2158c740dbcb9c5.css → ghost-dark-1e5bec737be037b1b5cb2a423603271d.css} +1 -1
  106. package/core/built/admin/index.html +5 -5
  107. package/core/frontend/helpers/get.js +89 -2
  108. package/core/frontend/public/robots.txt +0 -1
  109. package/core/frontend/services/rendering/renderer.js +19 -1
  110. package/core/frontend/services/theme-engine/i18n/I18n.js +7 -6
  111. package/core/frontend/services/theme-engine/i18n/ThemeI18n.js +4 -3
  112. package/core/server/adapters/storage/LocalStorageBase.js +13 -1
  113. package/core/server/api/endpoints/images.js +14 -3
  114. package/core/server/api/endpoints/utils/serializers/input/tiers.js +30 -2
  115. package/core/server/services/auth/api-key/admin.js +92 -85
  116. package/core/server/services/members/service.js +3 -1
  117. package/core/server/services/slack-notifications/service.js +6 -3
  118. package/core/server/services/themes/installer.js +1 -1
  119. package/core/server/services/tiers/TierRepository.js +2 -1
  120. package/core/server/web/admin/app.js +18 -1
  121. package/core/server/web/api/endpoints/admin/app.js +10 -0
  122. package/core/shared/labs.js +1 -0
  123. package/package.json +151 -149
  124. package/yarn.lock +517 -311
  125. package/components/tryghost-announcement-bar-settings-5.80.2.tgz +0 -0
  126. package/components/tryghost-audience-feedback-5.80.2.tgz +0 -0
  127. package/components/tryghost-bookshelf-repository-5.80.2.tgz +0 -0
  128. package/components/tryghost-collections-5.80.2.tgz +0 -0
  129. package/components/tryghost-custom-theme-settings-service-5.80.2.tgz +0 -0
  130. package/components/tryghost-domain-events-5.80.2.tgz +0 -0
  131. package/components/tryghost-dynamic-routing-events-5.80.2.tgz +0 -0
  132. package/components/tryghost-email-addresses-5.80.2.tgz +0 -0
  133. package/components/tryghost-email-analytics-provider-mailgun-5.80.2.tgz +0 -0
  134. package/components/tryghost-email-analytics-service-5.80.2.tgz +0 -0
  135. package/components/tryghost-email-content-generator-5.80.2.tgz +0 -0
  136. package/components/tryghost-email-service-5.80.2.tgz +0 -0
  137. package/components/tryghost-email-suppression-list-5.80.2.tgz +0 -0
  138. package/components/tryghost-external-media-inliner-5.80.2.tgz +0 -0
  139. package/components/tryghost-extract-api-key-5.80.2.tgz +0 -0
  140. package/components/tryghost-i18n-5.80.2.tgz +0 -0
  141. package/components/tryghost-importer-handler-content-files-5.80.2.tgz +0 -0
  142. package/components/tryghost-importer-revue-5.80.2.tgz +0 -0
  143. package/components/tryghost-in-memory-repository-5.80.2.tgz +0 -0
  144. package/components/tryghost-job-manager-5.80.2.tgz +0 -0
  145. package/components/tryghost-link-redirects-5.80.2.tgz +0 -0
  146. package/components/tryghost-link-replacer-5.80.2.tgz +0 -0
  147. package/components/tryghost-link-tracking-5.80.2.tgz +0 -0
  148. package/components/tryghost-magic-link-5.80.2.tgz +0 -0
  149. package/components/tryghost-mail-events-5.80.2.tgz +0 -0
  150. package/components/tryghost-mailgun-client-5.80.2.tgz +0 -0
  151. package/components/tryghost-members-api-5.80.2.tgz +0 -0
  152. package/components/tryghost-members-importer-5.80.2.tgz +0 -0
  153. package/components/tryghost-members-payments-5.80.2.tgz +0 -0
  154. package/components/tryghost-members-ssr-5.80.2.tgz +0 -0
  155. package/components/tryghost-mentions-email-report-5.80.2.tgz +0 -0
  156. package/components/tryghost-milestones-5.80.2.tgz +0 -0
  157. package/components/tryghost-minifier-5.80.2.tgz +0 -0
  158. package/components/tryghost-model-to-domain-event-interceptor-5.80.2.tgz +0 -0
  159. package/components/tryghost-mw-api-version-mismatch-5.80.2.tgz +0 -0
  160. package/components/tryghost-mw-cache-control-5.80.2.tgz +0 -0
  161. package/components/tryghost-mw-version-match-5.80.2.tgz +0 -0
  162. package/components/tryghost-mw-vhost-5.80.2.tgz +0 -0
  163. package/components/tryghost-nql-filter-expansions-5.80.2.tgz +0 -0
  164. package/components/tryghost-oembed-service-5.80.2.tgz +0 -0
  165. package/components/tryghost-package-json-5.80.2.tgz +0 -0
  166. package/components/tryghost-posts-service-5.80.2.tgz +0 -0
  167. package/components/tryghost-recommendations-5.80.2.tgz +0 -0
  168. package/components/tryghost-referrers-5.80.2.tgz +0 -0
  169. package/components/tryghost-session-service-5.80.2.tgz +0 -0
  170. package/components/tryghost-settings-path-manager-5.80.2.tgz +0 -0
  171. package/components/tryghost-slack-notifications-5.80.2.tgz +0 -0
  172. package/components/tryghost-tiers-5.80.2.tgz +0 -0
  173. package/components/tryghost-verification-trigger-5.80.2.tgz +0 -0
  174. package/components/tryghost-webmentions-5.80.2.tgz +0 -0
  175. /package/core/built/admin/assets/{chunk.763.bd01d249eecdcce3856c.js.LICENSE.txt → chunk.763.7849440ec6494d2449e9.js.LICENSE.txt} +0 -0
@@ -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%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%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.80%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%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%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22c1a8c8a94f%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%22d3a1c163cf%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2245e586d0c0%22%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%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.80%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%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%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22c1a8c8a94f%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%2224dcc6aee2%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2233f4aa4a54%22%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-0ede59da8efb5e28fa929557f7ff7154.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-ab71ce6991cc9bdcd6b915a9c984fa27.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-71cfcdf9410af9b3aec11a28f9a4130f.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -57,8 +57,8 @@
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
59
  <script src="assets/vendor-43a631b7c834235c4ff0b2186b5ca27d.js"></script>
60
- <script src="assets/chunk.763.bd01d249eecdcce3856c.js"></script>
61
- <script src="assets/chunk.524.c3472d95b4bd1a45a18d.js"></script>
62
- <script src="assets/ghost-ea7a51891dfca9b7a04aacfffe7dea4e.js"></script>
60
+ <script src="assets/chunk.763.7849440ec6494d2449e9.js"></script>
61
+ <script src="assets/chunk.524.c6ed93ff44a9cb49235c.js"></script>
62
+ <script src="assets/ghost-594a7dd26bdb4fec210b285cab5555cb.js"></script>
63
63
  </body>
64
64
  </html>
@@ -11,6 +11,7 @@ const Sentry = require('@sentry/node');
11
11
 
12
12
  const _ = require('lodash');
13
13
  const jsonpath = require('jsonpath');
14
+ const nqlLang = require('@tryghost/nql-lang');
14
15
 
15
16
  const messages = {
16
17
  mustBeCalledAsBlock: 'The {\\{{helperName}}} helper must be called as a block. E.g. {{#{helperName}}}...{{/{helperName}}}',
@@ -121,6 +122,84 @@ function parseOptions(globals, data, options) {
121
122
  return options;
122
123
  }
123
124
 
125
+ function optimiseFilterCacheability(resource, options) {
126
+ const noOptimisation = {
127
+ options,
128
+ parseResult(result) {
129
+ return result;
130
+ }
131
+ };
132
+ if (resource !== 'posts') {
133
+ return noOptimisation;
134
+ }
135
+
136
+ if (!options.filter) {
137
+ return noOptimisation;
138
+ }
139
+
140
+ try {
141
+ if (options.filter.split('id:-').length !== 2) {
142
+ return noOptimisation;
143
+ }
144
+
145
+ const parsedFilter = nqlLang.parse(options.filter);
146
+ // Support either `id:blah` or `id:blah+other:stuff`
147
+ if (!parsedFilter.$and && !parsedFilter.id) {
148
+ return noOptimisation;
149
+ }
150
+ const queries = parsedFilter.$and || [parsedFilter];
151
+ const query = queries.find((q) => {
152
+ return q?.id?.$ne;
153
+ });
154
+
155
+ if (!query) {
156
+ return noOptimisation;
157
+ }
158
+
159
+ const idToFilter = query.id.$ne;
160
+
161
+ let limit = options.limit;
162
+ if (options.limit !== 'all') {
163
+ limit = options.limit ? 1 + parseInt(options.limit, 10) : 16;
164
+ }
165
+
166
+ // We replace with id:-null so we don't have to deal with leading/trailing AND operators
167
+ const filter = options.filter.replace(/id:-[a-f0-9A-F]{24}/, 'id:-null');
168
+
169
+ const parseResult = function parseResult(result) {
170
+ const filteredPosts = result?.posts?.filter((post) => {
171
+ return post.id !== idToFilter;
172
+ }) || [];
173
+
174
+ const modifiedResult = {
175
+ ...result,
176
+ posts: limit === 'all' ? filteredPosts : filteredPosts.slice(0, limit - 1)
177
+ };
178
+
179
+ modifiedResult.meta = modifiedResult.meta || {};
180
+ modifiedResult.meta.cacheabilityOptimisation = true;
181
+
182
+ if (typeof modifiedResult?.meta?.pagination?.limit === 'number') {
183
+ modifiedResult.meta.pagination.limit = modifiedResult.meta.pagination.limit - 1;
184
+ }
185
+
186
+ return modifiedResult;
187
+ };
188
+
189
+ return {
190
+ options: {
191
+ ...options,
192
+ limit,
193
+ filter
194
+ },
195
+ parseResult
196
+ };
197
+ } catch (err) {
198
+ logging.warn(err);
199
+ return noOptimisation;
200
+ }
201
+ }
202
+
124
203
  /**
125
204
  *
126
205
  * @param {String} resource
@@ -131,6 +210,12 @@ function parseOptions(globals, data, options) {
131
210
  */
132
211
  async function makeAPICall(resource, controllerName, action, apiOptions) {
133
212
  const controller = api[controllerName];
213
+ let makeRequest = options => controller[action](options);
214
+
215
+ const {
216
+ options,
217
+ parseResult
218
+ } = optimiseFilterCacheability(resource, apiOptions);
134
219
 
135
220
  let timer;
136
221
 
@@ -141,7 +226,7 @@ async function makeAPICall(resource, controllerName, action, apiOptions) {
141
226
  const logLevel = config.get('optimization:getHelper:timeout:level') || 'error';
142
227
  const threshold = config.get('optimization:getHelper:timeout:threshold');
143
228
 
144
- const apiResponse = controller[action](apiOptions);
229
+ const apiResponse = makeRequest(options).then(parseResult);
145
230
 
146
231
  const timeout = new Promise((resolve) => {
147
232
  timer = setTimeout(() => {
@@ -161,7 +246,7 @@ async function makeAPICall(resource, controllerName, action, apiOptions) {
161
246
  response = await Promise.race([apiResponse, timeout]);
162
247
  clearTimeout(timer);
163
248
  } else {
164
- response = await controller[action](apiOptions);
249
+ response = await makeRequest(options).then(parseResult);
165
250
  }
166
251
 
167
252
  return response;
@@ -279,3 +364,5 @@ module.exports = async function get(resource, options) {
279
364
  };
280
365
 
281
366
  module.exports.async = true;
367
+
368
+ module.exports.optimiseFilterCacheability = optimiseFilterCacheability;
@@ -1,7 +1,6 @@
1
1
  User-agent: *
2
2
  Sitemap: {{blog-url}}/sitemap.xml
3
3
  Disallow: /ghost/
4
- Disallow: /p/
5
4
  Disallow: /email/
6
5
  Disallow: /r/
7
6
  Disallow: /webmentions/receive/
@@ -1,6 +1,12 @@
1
+ const path = require('path');
1
2
  const debug = require('@tryghost/debug')('services:routing:renderer:renderer');
3
+ const {IncorrectUsageError} = require('@tryghost/errors');
2
4
  const setContext = require('./context');
3
5
  const templates = require('./templates');
6
+ const tpl = require('@tryghost/tpl');
7
+ const messages = {
8
+ couldNotReadFile: 'Could not read file {file}'
9
+ };
4
10
 
5
11
  /**
6
12
  * @description Helper function to finally render the data.
@@ -26,5 +32,17 @@ module.exports = function renderer(req, res, data) {
26
32
  }
27
33
 
28
34
  // Render Call
29
- res.render(res._template, data);
35
+ res.render(res._template, data, function (err, html) {
36
+ if (err) {
37
+ if (err.code === 'ENOENT') {
38
+ return req.next(
39
+ new IncorrectUsageError({
40
+ message: tpl(messages.couldNotReadFile, {file: path.basename(err.path)})
41
+ })
42
+ );
43
+ }
44
+ return req.next(err);
45
+ }
46
+ res.send(html);
47
+ });
30
48
  };
@@ -14,9 +14,9 @@ const get = require('lodash/get');
14
14
  class I18n {
15
15
  /**
16
16
  * @param {object} [options]
17
- * @param {string} basePath - the base path to the translations directory
18
- * @param {string} [locale] - a locale string
19
- * @param {{dot|fulltext}} [stringMode] - which mode our translation keys use
17
+ * @param {string} options.basePath - the base path to the translations directory
18
+ * @param {string} [options.locale] - a locale string
19
+ * @param {string} [options.stringMode] - which mode our translation keys use
20
20
  */
21
21
  constructor(options = {}) {
22
22
  this._basePath = options.basePath || __dirname;
@@ -100,7 +100,7 @@ class I18n {
100
100
  /**
101
101
  * Attempt to load strings from a file
102
102
  *
103
- * @param {sting} [locale]
103
+ * @param {string} [locale]
104
104
  * @returns {object} strings
105
105
  */
106
106
  _loadStrings(locale) {
@@ -110,7 +110,7 @@ class I18n {
110
110
  return this._readTranslationsFile(locale);
111
111
  } catch (err) {
112
112
  if (err.code === 'ENOENT') {
113
- this._handleMissingFileError(locale, err);
113
+ this._handleMissingFileError(locale);
114
114
 
115
115
  if (locale !== this.defaultLocale()) {
116
116
  this._handleFallbackToDefault();
@@ -207,7 +207,7 @@ class I18n {
207
207
  */
208
208
  _readTranslationsFile(locale) {
209
209
  const filePath = path.join(...this._translationFileDirs(), this._translationFileName(locale));
210
- const content = fs.readFileSync(filePath);
210
+ const content = fs.readFileSync(filePath, 'utf8');
211
211
  return JSON.parse(content);
212
212
  }
213
213
 
@@ -276,6 +276,7 @@ class I18n {
276
276
  _handleMissingFileError(locale) {
277
277
  logging.warn(`i18n was unable to find ${locale}.json.`);
278
278
  }
279
+
279
280
  _handleInvalidFileError(locale, err) {
280
281
  logging.error(new errors.IncorrectUsageError({
281
282
  err,
@@ -4,9 +4,9 @@ const I18n = require('./I18n');
4
4
 
5
5
  class ThemeI18n extends I18n {
6
6
  /**
7
- * @param {objec} [options]
8
- * @param {string} basePath - the base path for the translation directory (e.g. where themes live)
9
- * @param {string} [locale] - a locale string
7
+ * @param {object} [options]
8
+ * @param {string} options.basePath - the base path for the translation directory (e.g. where themes live)
9
+ * @param {string} [options.locale] - a locale string
10
10
  */
11
11
  constructor(options = {}) {
12
12
  super(options);
@@ -48,6 +48,7 @@ class ThemeI18n extends I18n {
48
48
  logging.warn(`Theme translations file locales/${locale}.json not found.`);
49
49
  }
50
50
  }
51
+
51
52
  _handleInvalidFileError(locale, err) {
52
53
  logging.error(new errors.IncorrectUsageError({
53
54
  err,
@@ -58,7 +58,15 @@ class LocalStorageBase extends StorageBase {
58
58
  targetFilename = filename;
59
59
  await fs.mkdirs(targetDir);
60
60
 
61
- await fs.copy(file.path, targetFilename);
61
+ try {
62
+ await fs.copy(file.path, targetFilename);
63
+ } catch (err) {
64
+ if (err.code === 'ENAMETOOLONG') {
65
+ throw new errors.BadRequestError({err});
66
+ }
67
+
68
+ throw err;
69
+ }
62
70
 
63
71
  // The src for the image must be in URI format, not a file system path, which in Windows uses \
64
72
  // For local file system storage can use relative path so add a slash
@@ -149,6 +157,10 @@ class LocalStorageBase extends StorageBase {
149
157
  return next(new errors.NoPermissionError({err: err}));
150
158
  }
151
159
 
160
+ if (err.name === 'RangeNotSatisfiableError') {
161
+ return next(new errors.RangeNotSatisfiableError({err}));
162
+ }
163
+
152
164
  return next(new errors.InternalServerError({err: err}));
153
165
  }
154
166
 
@@ -1,8 +1,10 @@
1
1
  /* eslint-disable ghost/ghost-custom/max-api-complexity */
2
- const storage = require('../../adapters/storage');
2
+ const path = require('path');
3
+ const errors = require('@tryghost/errors');
3
4
  const imageTransform = require('@tryghost/image-transform');
5
+
6
+ const storage = require('../../adapters/storage');
4
7
  const config = require('../../../shared/config');
5
- const path = require('path');
6
8
 
7
9
  module.exports = {
8
10
  docName: 'images',
@@ -33,7 +35,16 @@ module.exports = {
33
35
  width: config.get('imageOptimization:defaultMaxWidth')
34
36
  }, imageOptimizationOptions);
35
37
 
36
- await imageTransform.resizeFromPath(options);
38
+ try {
39
+ await imageTransform.resizeFromPath(options);
40
+ } catch (err) {
41
+ // If the image processing fails, we don't want to store the image because it's corrupted/invalid
42
+ throw new errors.BadRequestError({
43
+ message: 'Image processing failed',
44
+ context: err.message,
45
+ help: 'Please verify that the image is valid'
46
+ });
47
+ }
37
48
 
38
49
  // Store the processed/optimized image
39
50
  const processedImageUrl = await store.save({
@@ -1,10 +1,26 @@
1
+ const {BadRequestError} = require('@tryghost/errors');
1
2
  const localUtils = require('../../index');
3
+ const nql = require('@tryghost/nql-lang');
4
+ const tpl = require('@tryghost/tpl');
5
+
6
+ const messages = {
7
+ invalidNQLFilter: 'The NQL filter you passed was invalid.'
8
+ };
2
9
 
3
10
  const forceActiveFilter = (frame) => {
4
11
  if (frame.options.filter) {
5
- frame.options.filter = `(${frame.options.filter})+active:true`;
12
+ frame.options.filter = {
13
+ $and: [
14
+ {
15
+ active: true
16
+ },
17
+ frame.options.filter
18
+ ]
19
+ };
6
20
  } else {
7
- frame.options.filter = 'active:true';
21
+ frame.options.filter = {
22
+ active: true
23
+ };
8
24
  }
9
25
  };
10
26
 
@@ -41,6 +57,18 @@ function convertTierInput(input) {
41
57
 
42
58
  module.exports = {
43
59
  all(_apiConfig, frame) {
60
+ if (frame.options.filter) {
61
+ try {
62
+ frame.options.filter = nql.parse(frame.options.filter);
63
+ } catch (err) {
64
+ throw new BadRequestError({
65
+ message: tpl(messages.invalidNQLFilter)
66
+ });
67
+ }
68
+ } else {
69
+ frame.options.filter = null;
70
+ }
71
+
44
72
  if (localUtils.isContentAPI(frame)) {
45
73
  // CASE: content api can only have active tiers
46
74
  forceActiveFilter(frame);
@@ -17,6 +17,7 @@ const messages = {
17
17
  };
18
18
 
19
19
  let JWT_OPTIONS_DEFAULTS = {
20
+ /** @type import('jsonwebtoken').Algorithm[] */
20
21
  algorithms: ['HS256'],
21
22
  maxAge: '5m'
22
23
  };
@@ -60,7 +61,7 @@ const authenticate = function apiKeyAdminAuth(req, res, next) {
60
61
  }));
61
62
  }
62
63
 
63
- return authenticateWithToken(req, res, next, {token, JWT_OPTIONS: JWT_OPTIONS_DEFAULTS});
64
+ return wrappedAuthenticateWithToken(req, res, next, {token});
64
65
  };
65
66
 
66
67
  const authenticateWithUrl = function apiKeyAuthenticateWithUrl(req, res, next) {
@@ -72,9 +73,20 @@ const authenticateWithUrl = function apiKeyAuthenticateWithUrl(req, res, next) {
72
73
  }));
73
74
  }
74
75
  // CASE: Scheduler publish URLs can have long maxAge but controllerd by expiry and neverBefore
75
- return authenticateWithToken(req, res, next, {token, JWT_OPTIONS: _.omit(JWT_OPTIONS_DEFAULTS, 'maxAge')});
76
+ return wrappedAuthenticateWithToken(req, res, next, {token, ignoreMaxAge: true});
76
77
  };
77
78
 
79
+ async function wrappedAuthenticateWithToken(req, res, next, options) {
80
+ try {
81
+ const {apiKey, user} = await authenticateWithToken(req.originalUrl, options.token, options.ignoreMaxAge);
82
+ req.api_key = apiKey;
83
+ req.user = user;
84
+ next();
85
+ } catch (err) {
86
+ next(err);
87
+ }
88
+ }
89
+
78
90
  /**
79
91
  * Admin API key authentication flow:
80
92
  * 1. extract the JWT token from the `Authorization: Ghost xxxx` header or from URL(for schedules)
@@ -89,109 +101,104 @@ const authenticateWithUrl = function apiKeyAuthenticateWithUrl(req, res, next) {
89
101
  * - the "Audience" claim should match the requested API path
90
102
  * https://tools.ietf.org/html/rfc7519#section-4.1.3
91
103
  */
92
- const authenticateWithToken = async function apiKeyAuthenticateWithToken(req, res, next, {token, JWT_OPTIONS}) {
104
+ const authenticateWithToken = async function apiKeyAuthenticateWithToken(originalUrl, token, ignoreMaxAge) {
93
105
  const decoded = jwt.decode(token, {complete: true});
106
+ const jwtValidationOptions = ignoreMaxAge ? _.omit(JWT_OPTIONS_DEFAULTS, 'maxAge') : JWT_OPTIONS_DEFAULTS;
94
107
 
95
108
  if (!decoded || !decoded.header) {
96
- return next(new errors.BadRequestError({
109
+ throw new errors.BadRequestError({
97
110
  message: tpl(messages.invalidToken),
98
111
  code: 'INVALID_JWT'
99
- }));
112
+ });
100
113
  }
101
114
 
102
115
  const apiKeyId = decoded.header.kid;
103
116
 
104
117
  if (!apiKeyId) {
105
- return next(new errors.BadRequestError({
118
+ throw new errors.BadRequestError({
106
119
  message: tpl(messages.adminApiKidMissing),
107
120
  code: 'MISSING_ADMIN_API_KID'
108
- }));
121
+ });
109
122
  }
110
123
 
111
- try {
112
- const apiKey = await models.ApiKey.findOne({id: apiKeyId}, {withRelated: ['integration']});
113
-
114
- if (!apiKey) {
115
- return next(new errors.UnauthorizedError({
116
- message: tpl(messages.unknownAdminApiKey),
117
- code: 'UNKNOWN_ADMIN_API_KEY'
118
- }));
119
- }
120
-
121
- if (apiKey.get('type') !== 'admin') {
122
- return next(new errors.UnauthorizedError({
123
- message: tpl(messages.invalidApiKeyType),
124
- code: 'INVALID_API_KEY_TYPE'
125
- }));
126
- }
127
-
128
- // CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
129
- if (limitService.isLimited('customIntegrations')
124
+ const apiKey = await models.ApiKey.findOne({id: apiKeyId}, {withRelated: ['integration']});
125
+
126
+ if (!apiKey) {
127
+ throw new errors.UnauthorizedError({
128
+ message: tpl(messages.unknownAdminApiKey),
129
+ code: 'UNKNOWN_ADMIN_API_KEY'
130
+ });
131
+ }
132
+
133
+ if (apiKey.get('type') !== 'admin') {
134
+ throw new errors.UnauthorizedError({
135
+ message: tpl(messages.invalidApiKeyType),
136
+ code: 'INVALID_API_KEY_TYPE'
137
+ });
138
+ }
139
+
140
+ // CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
141
+ if (limitService.isLimited('customIntegrations')
130
142
  && (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
131
- // NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
132
- // a concept of measuring if the limit has been surpassed
133
- await limitService.errorIfWouldGoOverLimit('customIntegrations');
134
- }
135
-
136
- // Decoding from hex and transforming into bytes is here to
137
- // keep comparison of the bytes that are stored in the secret.
138
- // Useful context:
139
- // https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138
140
- const secret = Buffer.from(apiKey.get('secret'), 'hex');
141
-
142
- // Using req.originalUrl means we get the right url even if version-rewrites have happened
143
- const {version, api} = legacyApiPathMatch(req.originalUrl);
144
-
145
- // ensure the token was meant for this api
146
- let options;
147
-
148
- if (version) {
149
- // CASE: legacy versioned api request
150
- options = Object.assign({
151
- audience: new RegExp(`/?${version}/${api}/?$`)
152
- }, JWT_OPTIONS);
153
- } else {
154
- options = Object.assign({
155
- audience: new RegExp(`/?${api}/?$`)
156
- }, JWT_OPTIONS);
157
- }
158
-
159
- try {
160
- jwt.verify(token, secret, options);
161
- } catch (err) {
162
- return next(new errors.UnauthorizedError({
163
- message: tpl(messages.invalidTokenWithMessage, {message: err.message}),
164
- code: 'INVALID_JWT',
165
- err
166
- }));
167
- }
168
-
169
- // authenticated OK
170
-
171
- if (apiKey.get('user_id')) {
172
- // fetch the user and store it on the request for later checks and logging
173
- const user = await models.User.findOne(
174
- {id: apiKey.get('user_id'), status: 'active'},
175
- {require: true}
176
- );
177
-
178
- req.user = user;
179
- }
180
-
181
- // store the api key on the request for later checks and logging
182
- req.api_key = apiKey;
143
+ // NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
144
+ // a concept of measuring if the limit has been surpassed
145
+ await limitService.errorIfWouldGoOverLimit('customIntegrations');
146
+ }
183
147
 
184
- next();
148
+ // Decoding from hex and transforming into bytes is here to
149
+ // keep comparison of the bytes that are stored in the secret.
150
+ // Useful context:
151
+ // https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138
152
+ const secret = Buffer.from(apiKey.get('secret'), 'hex');
153
+
154
+ // Using req.originalUrl means we get the right url even if version-rewrites have happened
155
+ const {version, api} = legacyApiPathMatch(originalUrl);
156
+
157
+ // ensure the token was meant for this api
158
+ let options;
159
+
160
+ if (version) {
161
+ // CASE: legacy versioned api request
162
+ options = Object.assign({
163
+ audience: new RegExp(`/?${version}/${api}/?$`)
164
+ }, jwtValidationOptions);
165
+ } else {
166
+ options = Object.assign({
167
+ audience: new RegExp(`/?${api}/?$`)
168
+ }, jwtValidationOptions);
169
+ }
170
+
171
+ try {
172
+ jwt.verify(token, secret, options);
185
173
  } catch (err) {
186
- if (err instanceof errors.HostLimitError) {
187
- next(err);
188
- } else {
189
- next(new errors.InternalServerError({err}));
190
- }
174
+ throw new errors.UnauthorizedError({
175
+ message: tpl(messages.invalidTokenWithMessage, {message: err.message}),
176
+ code: 'INVALID_JWT',
177
+ err
178
+ });
179
+ }
180
+
181
+ // authenticated OK
182
+ let result = {
183
+ user: null,
184
+ apiKey: apiKey
185
+ };
186
+
187
+ if (apiKey.get('user_id')) {
188
+ // fetch the user and store it on the request for later checks and logging
189
+ const user = await models.User.findOne(
190
+ {id: apiKey.get('user_id'), status: 'active'},
191
+ {require: true}
192
+ );
193
+
194
+ result.user = user;
191
195
  }
196
+
197
+ return result;
192
198
  };
193
199
 
194
200
  module.exports = {
195
201
  authenticate,
196
- authenticateWithUrl
202
+ authenticateWithUrl,
203
+ authenticateWithToken
197
204
  };
@@ -55,7 +55,9 @@ const initMembersCSVImporter = ({stripeAPIService}) => {
55
55
  },
56
56
  getTierByName: async (name) => {
57
57
  const tiers = await tiersService.api.browse({
58
- filter: `name:'${name}'`
58
+ filter: {
59
+ name
60
+ }
59
61
  });
60
62
 
61
63
  if (tiers.data.length > 0) {
@@ -12,10 +12,11 @@ class SlackNotificationsServiceWrapper {
12
12
  * @param {string} deps.siteUrl
13
13
  * @param {boolean} deps.isEnabled
14
14
  * @param {URL} deps.webhookUrl
15
+ * @param {number} deps.minThreshold
15
16
  *
16
17
  * @returns {import('@tryghost/slack-notifications/lib/SlackNotificationsService')}
17
18
  */
18
- static create({siteUrl, isEnabled, webhookUrl}) {
19
+ static create({siteUrl, isEnabled, webhookUrl, minThreshold}) {
19
20
  const {
20
21
  SlackNotificationsService,
21
22
  SlackNotifications
@@ -32,7 +33,8 @@ class SlackNotificationsServiceWrapper {
32
33
  logging,
33
34
  config: {
34
35
  isEnabled,
35
- webhookUrl
36
+ webhookUrl,
37
+ minThreshold
36
38
  },
37
39
  slackNotifications
38
40
  });
@@ -49,8 +51,9 @@ class SlackNotificationsServiceWrapper {
49
51
  const siteUrl = urlUtils.getSiteUrl();
50
52
  const isEnabled = !!(hostSettings?.milestones?.enabled && hostSettings?.milestones?.url);
51
53
  const webhookUrl = hostSettings?.milestones?.url;
54
+ const minThreshold = hostSettings?.milestones?.minThreshold ? parseInt(hostSettings.milestones.minThreshold) : 0;
52
55
 
53
- this.#api = SlackNotificationsServiceWrapper.create({siteUrl, isEnabled, webhookUrl});
56
+ this.#api = SlackNotificationsServiceWrapper.create({siteUrl, isEnabled, webhookUrl, minThreshold});
54
57
 
55
58
  this.#api.subscribeEvents();
56
59
  }
@@ -3,7 +3,7 @@ const os = require('os');
3
3
  const path = require('path');
4
4
  const security = require('@tryghost/security');
5
5
  const request = require('@tryghost/request');
6
- const errors = require('@tryghost/errors/lib/errors');
6
+ const errors = require('@tryghost/errors');
7
7
  const limitService = require('../../services/limits');
8
8
  const {setFromZip} = require('./storage');
9
9
 
@@ -86,7 +86,8 @@ module.exports = class TierRepository {
86
86
  * @returns {Promise<import('@tryghost/tiers/lib/Tier')[]>}
87
87
  */
88
88
  async getAll(options = {}) {
89
- const filter = nql(options.filter, {});
89
+ const filter = nql();
90
+ filter.filter = options.filter || {};
90
91
  return Promise.all(this.#store.slice().filter((item) => {
91
92
  return filter.queryJSON(this.toPrimitive(item));
92
93
  }).map((tier) => {