ghost 5.36.1 → 5.38.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 (208) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.38.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.38.0.tgz +0 -0
  3. package/components/tryghost-adapter-manager-5.38.0.tgz +0 -0
  4. package/components/tryghost-api-framework-5.38.0.tgz +0 -0
  5. package/components/tryghost-api-version-compatibility-service-5.38.0.tgz +0 -0
  6. package/components/tryghost-audience-feedback-5.38.0.tgz +0 -0
  7. package/components/tryghost-bootstrap-socket-5.38.0.tgz +0 -0
  8. package/components/tryghost-constants-5.38.0.tgz +0 -0
  9. package/components/tryghost-custom-theme-settings-service-5.38.0.tgz +0 -0
  10. package/components/tryghost-data-generator-5.38.0.tgz +0 -0
  11. package/components/tryghost-domain-events-5.38.0.tgz +0 -0
  12. package/components/tryghost-dynamic-routing-events-5.38.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-provider-mailgun-5.38.0.tgz +0 -0
  14. package/components/tryghost-email-analytics-service-5.38.0.tgz +0 -0
  15. package/components/{tryghost-email-content-generator-5.36.1.tgz → tryghost-email-content-generator-5.38.0.tgz} +0 -0
  16. package/components/tryghost-email-events-5.38.0.tgz +0 -0
  17. package/components/tryghost-email-service-5.38.0.tgz +0 -0
  18. package/components/tryghost-email-suppression-list-5.38.0.tgz +0 -0
  19. package/components/tryghost-event-aware-cache-wrapper-5.38.0.tgz +0 -0
  20. package/components/{tryghost-express-dynamic-redirects-5.36.1.tgz → tryghost-express-dynamic-redirects-5.38.0.tgz} +0 -0
  21. package/components/tryghost-external-media-inliner-5.38.0.tgz +0 -0
  22. package/components/tryghost-extract-api-key-5.38.0.tgz +0 -0
  23. package/components/tryghost-html-to-plaintext-5.38.0.tgz +0 -0
  24. package/components/tryghost-i18n-5.38.0.tgz +0 -0
  25. package/components/tryghost-importer-handler-content-files-5.38.0.tgz +0 -0
  26. package/components/tryghost-importer-revue-5.38.0.tgz +0 -0
  27. package/components/tryghost-job-manager-5.38.0.tgz +0 -0
  28. package/components/tryghost-link-redirects-5.38.0.tgz +0 -0
  29. package/components/tryghost-link-replacer-5.38.0.tgz +0 -0
  30. package/components/tryghost-link-tracking-5.38.0.tgz +0 -0
  31. package/components/tryghost-magic-link-5.38.0.tgz +0 -0
  32. package/components/tryghost-mailgun-client-5.38.0.tgz +0 -0
  33. package/components/tryghost-member-attribution-5.38.0.tgz +0 -0
  34. package/components/tryghost-member-events-5.38.0.tgz +0 -0
  35. package/components/tryghost-members-api-5.38.0.tgz +0 -0
  36. package/components/tryghost-members-csv-5.38.0.tgz +0 -0
  37. package/components/{tryghost-members-events-service-5.36.1.tgz → tryghost-members-events-service-5.38.0.tgz} +0 -0
  38. package/components/tryghost-members-importer-5.38.0.tgz +0 -0
  39. package/components/tryghost-members-offers-5.38.0.tgz +0 -0
  40. package/components/tryghost-members-payments-5.38.0.tgz +0 -0
  41. package/components/tryghost-members-ssr-5.38.0.tgz +0 -0
  42. package/components/tryghost-members-stripe-service-5.38.0.tgz +0 -0
  43. package/components/tryghost-milestones-5.38.0.tgz +0 -0
  44. package/components/tryghost-minifier-5.38.0.tgz +0 -0
  45. package/components/tryghost-mw-api-version-mismatch-5.38.0.tgz +0 -0
  46. package/components/tryghost-mw-cache-control-5.38.0.tgz +0 -0
  47. package/components/tryghost-mw-error-handler-5.38.0.tgz +0 -0
  48. package/components/tryghost-mw-session-from-token-5.38.0.tgz +0 -0
  49. package/components/tryghost-mw-update-user-last-seen-5.38.0.tgz +0 -0
  50. package/components/tryghost-mw-version-match-5.38.0.tgz +0 -0
  51. package/components/tryghost-mw-vhost-5.38.0.tgz +0 -0
  52. package/components/tryghost-oembed-service-5.38.0.tgz +0 -0
  53. package/components/tryghost-package-json-5.38.0.tgz +0 -0
  54. package/components/tryghost-referrers-5.38.0.tgz +0 -0
  55. package/components/tryghost-security-5.38.0.tgz +0 -0
  56. package/components/{tryghost-session-service-5.36.1.tgz → tryghost-session-service-5.38.0.tgz} +0 -0
  57. package/components/tryghost-settings-path-manager-5.38.0.tgz +0 -0
  58. package/components/tryghost-slack-notifications-5.38.0.tgz +0 -0
  59. package/components/tryghost-staff-service-5.38.0.tgz +0 -0
  60. package/components/tryghost-stats-service-5.38.0.tgz +0 -0
  61. package/components/tryghost-tiers-5.38.0.tgz +0 -0
  62. package/components/tryghost-update-check-service-5.38.0.tgz +0 -0
  63. package/components/tryghost-verification-trigger-5.38.0.tgz +0 -0
  64. package/components/tryghost-version-notifications-data-service-5.38.0.tgz +0 -0
  65. package/components/tryghost-webmentions-5.38.0.tgz +0 -0
  66. package/content/themes/casper/assets/built/screen.css +1 -1
  67. package/content/themes/casper/assets/built/screen.css.map +1 -1
  68. package/content/themes/casper/assets/css/screen.css +44 -30
  69. package/content/themes/casper/default.hbs +2 -2
  70. package/content/themes/casper/error.hbs +2 -2
  71. package/content/themes/casper/package.json +1 -1
  72. package/core/boot.js +15 -6
  73. package/core/built/admin/assets/chunk.143.c6802c882a911797ce4f.js +49 -0
  74. package/core/built/admin/assets/chunk.178.09faefd4027fcba4113d.js +10 -0
  75. package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js → chunk.220.9ca2950240aba3fced21.js} +2168 -2035
  76. package/core/built/admin/assets/{chunk.79.53e8aa9671b2d5dae8ba.js → chunk.79.acb7dd01e1c785f4920c.js} +191 -183
  77. package/core/built/admin/assets/{ghost-b828e9e3c161aae92909c2e163656bb1.js → ghost-35103ff053c43f1dfa7f35821c3c2412.js} +271 -249
  78. package/core/built/admin/assets/ghost-a9307c9cfe26a4bc621e02cd3bae421a.css +1 -0
  79. package/core/built/admin/assets/ghost-dark-f309cf445255344e4861a95ecb8f1920.css +1 -0
  80. package/core/built/admin/assets/{vendor-c4684647d4f5213e5dbb6763de430e7e.js → vendor-b982e3bf1020bff77b2a3c44d5f59e55.js} +442 -457
  81. package/core/built/admin/index.html +6 -6
  82. package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -5
  83. package/core/frontend/helpers/ghost_head.js +4 -1
  84. package/core/frontend/meta/asset-url.js +9 -0
  85. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  86. package/core/frontend/services/sitemap/base-generator.js +5 -1
  87. package/core/server/adapters/storage/LocalImagesStorage.js +1 -1
  88. package/core/server/api/endpoints/db.js +17 -0
  89. package/core/server/api/endpoints/email-previews.js +2 -43
  90. package/core/server/api/endpoints/emails.js +1 -22
  91. package/core/server/api/endpoints/mentions.js +2 -1
  92. package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +14 -8
  93. package/core/server/api/endpoints/utils/serializers/output/posts.js +2 -2
  94. package/core/server/data/db/backup.js +13 -13
  95. package/core/server/data/importer/handlers/image.js +2 -2
  96. package/core/server/data/importer/import-manager.js +64 -5
  97. package/core/server/data/importer/importers/ContentFileImporter.js +128 -0
  98. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -1
  99. package/core/server/data/schema/commands.js +21 -10
  100. package/core/server/lib/common/events.js +16 -23
  101. package/core/server/models/base/plugins/relations.js +5 -3
  102. package/core/server/models/index.js +5 -0
  103. package/core/server/models/mention.js +13 -0
  104. package/core/server/run-update-check.js +3 -1
  105. package/core/server/services/comments/emails.js +2 -2
  106. package/core/server/services/email-service/wrapper.js +2 -0
  107. package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
  108. package/core/server/services/media-inliner/index.js +1 -0
  109. package/core/server/services/media-inliner/service.js +62 -0
  110. package/core/server/services/members/stats/members-stats.js +13 -9
  111. package/core/server/services/mentions/BookshelfMentionRepository.js +12 -1
  112. package/core/server/services/mentions/MentionController.js +7 -1
  113. package/core/server/services/mentions/WebmentionMetadata.js +3 -2
  114. package/core/server/services/mentions/service.js +5 -3
  115. package/core/server/services/posts/posts-service.js +3 -14
  116. package/core/server/{analytics-events.js → services/segment/index.js} +4 -3
  117. package/core/server/services/staff/index.js +2 -0
  118. package/core/server/services/stripe/config.js +4 -0
  119. package/core/server/services/url/Urls.js +10 -2
  120. package/core/server/update-check.js +5 -3
  121. package/core/server/web/api/endpoints/admin/app.js +5 -4
  122. package/core/server/web/api/endpoints/admin/routes.js +6 -0
  123. package/core/server/web/api/middleware/index.js +1 -2
  124. package/core/shared/config/overrides.json +34 -0
  125. package/core/shared/labs.js +3 -3
  126. package/package.json +164 -159
  127. package/yarn.lock +887 -934
  128. package/components/tryghost-adapter-cache-memory-ttl-5.36.1.tgz +0 -0
  129. package/components/tryghost-adapter-cache-redis-5.36.1.tgz +0 -0
  130. package/components/tryghost-adapter-manager-5.36.1.tgz +0 -0
  131. package/components/tryghost-api-framework-5.36.1.tgz +0 -0
  132. package/components/tryghost-api-version-compatibility-service-5.36.1.tgz +0 -0
  133. package/components/tryghost-audience-feedback-5.36.1.tgz +0 -0
  134. package/components/tryghost-bootstrap-socket-5.36.1.tgz +0 -0
  135. package/components/tryghost-constants-5.36.1.tgz +0 -0
  136. package/components/tryghost-custom-theme-settings-service-5.36.1.tgz +0 -0
  137. package/components/tryghost-data-generator-5.36.1.tgz +0 -0
  138. package/components/tryghost-domain-events-5.36.1.tgz +0 -0
  139. package/components/tryghost-dynamic-routing-events-5.36.1.tgz +0 -0
  140. package/components/tryghost-email-analytics-provider-mailgun-5.36.1.tgz +0 -0
  141. package/components/tryghost-email-analytics-service-5.36.1.tgz +0 -0
  142. package/components/tryghost-email-events-5.36.1.tgz +0 -0
  143. package/components/tryghost-email-service-5.36.1.tgz +0 -0
  144. package/components/tryghost-email-suppression-list-5.36.1.tgz +0 -0
  145. package/components/tryghost-event-aware-cache-wrapper-5.36.1.tgz +0 -0
  146. package/components/tryghost-extract-api-key-5.36.1.tgz +0 -0
  147. package/components/tryghost-html-to-plaintext-5.36.1.tgz +0 -0
  148. package/components/tryghost-i18n-5.36.1.tgz +0 -0
  149. package/components/tryghost-importer-revue-5.36.1.tgz +0 -0
  150. package/components/tryghost-job-manager-5.36.1.tgz +0 -0
  151. package/components/tryghost-link-redirects-5.36.1.tgz +0 -0
  152. package/components/tryghost-link-replacer-5.36.1.tgz +0 -0
  153. package/components/tryghost-link-tracking-5.36.1.tgz +0 -0
  154. package/components/tryghost-magic-link-5.36.1.tgz +0 -0
  155. package/components/tryghost-mailgun-client-5.36.1.tgz +0 -0
  156. package/components/tryghost-member-attribution-5.36.1.tgz +0 -0
  157. package/components/tryghost-member-events-5.36.1.tgz +0 -0
  158. package/components/tryghost-members-api-5.36.1.tgz +0 -0
  159. package/components/tryghost-members-csv-5.36.1.tgz +0 -0
  160. package/components/tryghost-members-importer-5.36.1.tgz +0 -0
  161. package/components/tryghost-members-offers-5.36.1.tgz +0 -0
  162. package/components/tryghost-members-payments-5.36.1.tgz +0 -0
  163. package/components/tryghost-members-ssr-5.36.1.tgz +0 -0
  164. package/components/tryghost-members-stripe-service-5.36.1.tgz +0 -0
  165. package/components/tryghost-milestones-5.36.1.tgz +0 -0
  166. package/components/tryghost-minifier-5.36.1.tgz +0 -0
  167. package/components/tryghost-mw-api-version-mismatch-5.36.1.tgz +0 -0
  168. package/components/tryghost-mw-cache-control-5.36.1.tgz +0 -0
  169. package/components/tryghost-mw-error-handler-5.36.1.tgz +0 -0
  170. package/components/tryghost-mw-session-from-token-5.36.1.tgz +0 -0
  171. package/components/tryghost-mw-update-user-last-seen-5.36.1.tgz +0 -0
  172. package/components/tryghost-mw-vhost-5.36.1.tgz +0 -0
  173. package/components/tryghost-oembed-service-5.36.1.tgz +0 -0
  174. package/components/tryghost-package-json-5.36.1.tgz +0 -0
  175. package/components/tryghost-referrers-5.36.1.tgz +0 -0
  176. package/components/tryghost-security-5.36.1.tgz +0 -0
  177. package/components/tryghost-settings-path-manager-5.36.1.tgz +0 -0
  178. package/components/tryghost-slack-notifications-5.36.1.tgz +0 -0
  179. package/components/tryghost-staff-service-5.36.1.tgz +0 -0
  180. package/components/tryghost-stats-service-5.36.1.tgz +0 -0
  181. package/components/tryghost-tiers-5.36.1.tgz +0 -0
  182. package/components/tryghost-update-check-service-5.36.1.tgz +0 -0
  183. package/components/tryghost-verification-trigger-5.36.1.tgz +0 -0
  184. package/components/tryghost-version-notifications-data-service-5.36.1.tgz +0 -0
  185. package/components/tryghost-webmentions-5.36.1.tgz +0 -0
  186. package/core/built/admin/assets/chunk.143.26ea9f26571d656653f0.js +0 -49
  187. package/core/built/admin/assets/chunk.178.dd71b3a764b73facc400.js +0 -11
  188. package/core/built/admin/assets/ghost-7ecf5c7934d90798485ee5ac2956f7fe.css +0 -1
  189. package/core/built/admin/assets/ghost-dark-e50717df8e57d3e7fee67a0bcea895ad.css +0 -1
  190. package/core/frontend/src/cards/css/before-after.css +0 -81
  191. package/core/frontend/src/cards/js/before-after.js +0 -36
  192. package/core/server/data/importer/importers/image.js +0 -76
  193. package/core/server/data/schema/clients/index.js +0 -7
  194. package/core/server/data/schema/clients/mysql.js +0 -34
  195. package/core/server/data/schema/clients/sqlite3.js +0 -39
  196. package/core/server/services/bulk-email/bulk-email-processor.js +0 -289
  197. package/core/server/services/bulk-email/index.js +0 -1
  198. package/core/server/services/mega/email-preview.js +0 -54
  199. package/core/server/services/mega/feedback-buttons.js +0 -66
  200. package/core/server/services/mega/index.js +0 -14
  201. package/core/server/services/mega/mega.js +0 -626
  202. package/core/server/services/mega/post-email-serializer.js +0 -559
  203. package/core/server/services/mega/segment-parser.js +0 -20
  204. package/core/server/services/mega/template.js +0 -1319
  205. package/core/server/services/mentions/WebmentionRequest.js +0 -20
  206. package/core/server/web/admin/middleware.js +0 -17
  207. package/core/server/web/api/middleware/version-match.js +0 -31
  208. /package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js.LICENSE.txt → chunk.220.9ca2950240aba3fced21.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%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.36%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%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.38%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%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-7ecf5c7934d90798485ee5ac2956f7fe.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-a9307c9cfe26a4bc621e02cd3bae421a.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-c4684647d4f5213e5dbb6763de430e7e.js"></script>
60
- <script src="assets/chunk.502.800e1515996bcc900013.js"></script>
61
- <script src="assets/chunk.143.26ea9f26571d656653f0.js"></script>
62
- <script src="assets/ghost-b828e9e3c161aae92909c2e163656bb1.js"></script>
59
+ <script src="assets/vendor-b982e3bf1020bff77b2a3c44d5f59e55.js"></script>
60
+ <script src="assets/chunk.220.9ca2950240aba3fced21.js"></script>
61
+ <script src="assets/chunk.143.c6802c882a911797ce4f.js"></script>
62
+ <script src="assets/ghost-35103ff053c43f1dfa7f35821c3c2412.js"></script>
63
63
  </body>
64
64
  </html>
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // Converts normal HTML into AMP HTML with Amperize module and uses a cache to return it from
8
8
  // there if available. The cacheId is a combination of `updated_at` and the `slug`.
9
- const {DateTime, Interval} = require('luxon');
9
+ const {DateTime} = require('luxon');
10
10
  const errors = require('@tryghost/errors');
11
11
  const logging = require('@tryghost/logging');
12
12
 
@@ -120,8 +120,6 @@ function getAmperizeHTML(html, post) {
120
120
 
121
121
  amperize = amperize || new Amperize();
122
122
 
123
- const startedAtMoment = DateTime.now();
124
-
125
123
  let cacheDateTime;
126
124
  let postDateTime;
127
125
 
@@ -136,8 +134,6 @@ function getAmperizeHTML(html, post) {
136
134
  if (!amperizeCache[post.id] || cacheDateTime.diff(postDateTime).valueOf() < 0) {
137
135
  return new Promise((resolve) => {
138
136
  amperize.parse(html, (err, res) => {
139
- logging.info('amp.parse', post.url, Interval.fromDateTimes(startedAtMoment, DateTime.now()).length('milliseconds') + 'ms');
140
-
141
137
  if (err) {
142
138
  if (err.src) {
143
139
  // This is a valid 500 GhostError because it means the amperize parser is unable to handle some Ghost HTML.
@@ -64,7 +64,10 @@ function getMembersHelper(data, frontendKey) {
64
64
  let membersHelper = `<script defer src="${scriptUrl}" ${dataAttributes} crossorigin="anonymous"></script>`;
65
65
  membersHelper += (`<style id="gh-members-styles">${templateStyles}</style>`);
66
66
  if (settingsCache.get('paid_members_enabled')) {
67
- membersHelper += '<script async src="https://js.stripe.com/v3/"></script>';
67
+ // disable fraud detection for e2e tests to reduce waiting time
68
+ const isFraudSignalsEnabled = process.env.NODE_ENV === 'testing-browser' ? '?advancedFraudSignals=false' : '';
69
+
70
+ membersHelper += `<script async src="https://js.stripe.com/v3/${isFraudSignalsEnabled}"></script>`;
68
71
  }
69
72
  return membersHelper;
70
73
  }
@@ -2,6 +2,7 @@ const crypto = require('crypto');
2
2
  const config = require('../../shared/config');
3
3
  const {blogIcon} = require('../../server/lib/image');
4
4
  const urlUtils = require('../../shared/url-utils');
5
+ const {SafeString} = require('../services/handlebars');
5
6
 
6
7
  /**
7
8
  * Serve either uploaded favicon or default
@@ -11,7 +12,15 @@ function getFaviconUrl() {
11
12
  return blogIcon.getIconUrl();
12
13
  }
13
14
 
15
+ /**
16
+ * Prepare URL for an asset
17
+ * @param {string|SafeString} path — the asset’s path
18
+ * @param {boolean} hasMinFile — flag for the existence of a minified version for the asset
19
+ * @returns {string}
20
+ */
14
21
  function getAssetUrl(path, hasMinFile) {
22
+ path = path instanceof SafeString ? path.string : path;
23
+
15
24
  // CASE: favicon - this is special path with its own functionality
16
25
  if (path.match(/\/?favicon\.(ico|png)$/)) {
17
26
  // @TODO, resolve this - we should only be resolving subdirectory and extension.
@@ -52,7 +52,7 @@ class StaticPagesRouter extends ParentRouter {
52
52
  }
53
53
 
54
54
  /**
55
- * @description Prepare context for futher middleware/controllers.
55
+ * @description Prepare context for further middleware/controllers.
56
56
  * @param {Object} req
57
57
  * @param {Object} res
58
58
  * @param {Function} next
@@ -22,6 +22,10 @@ class BaseSiteMapGenerator {
22
22
  this.maxPerPage = 50000;
23
23
  }
24
24
 
25
+ hasCanonicalUrl(datum) {
26
+ return Boolean(datum?.canonical_url);
27
+ }
28
+
25
29
  generateXmlFromNodes(page) {
26
30
  // Get a mapping of node to timestamp
27
31
  let nodesToProcess = _.map(this.nodeLookup, (node, id) => {
@@ -75,7 +79,7 @@ class BaseSiteMapGenerator {
75
79
  addUrl(url, datum) {
76
80
  const node = this.createUrlNodeFromDatum(url, datum);
77
81
 
78
- if (node) {
82
+ if (node && !this.hasCanonicalUrl(datum)) {
79
83
  this.updateLastModified(datum);
80
84
  this.updateLookups(datum, node);
81
85
  // force regeneration of xml
@@ -27,7 +27,7 @@ class LocalImagesStorage extends LocalStorageBase {
27
27
  /**
28
28
  * Saves a buffer in the targetPath
29
29
  * @param {Buffer} buffer is an instance of Buffer
30
- * @param {String} targetPath path to which the buffer should be written
30
+ * @param {String} targetPath relative path NOT including storage path to which the buffer should be written
31
31
  * @returns {Promise<String>} a URL to retrieve the data
32
32
  */
33
33
  async saveRaw(buffer, targetPath) {
@@ -3,6 +3,7 @@ const moment = require('moment-timezone');
3
3
  const dbBackup = require('../../data/db/backup');
4
4
  const exporter = require('../../data/exporter');
5
5
  const importer = require('../../data/importer');
6
+ const mediaInliner = require('../../services/media-inliner');
6
7
  const errors = require('@tryghost/errors');
7
8
  const models = require('../../models');
8
9
  const settingsCache = require('../../../shared/settings-cache');
@@ -93,6 +94,22 @@ module.exports = {
93
94
  }
94
95
  },
95
96
 
97
+ inlineMedia: {
98
+ permissions: {
99
+ method: 'importContent'
100
+ },
101
+ validation: {
102
+ options: {
103
+ include: {
104
+ values: ['domains']
105
+ }
106
+ }
107
+ },
108
+ async query(frame) {
109
+ return mediaInliner.api.startMediaInliner(frame.data.domains);
110
+ }
111
+ },
112
+
96
113
  deleteAllContent: {
97
114
  headers: {
98
115
  cacheInvalidate: true
@@ -1,14 +1,4 @@
1
- const models = require('../../models');
2
- const tpl = require('@tryghost/tpl');
3
- const errors = require('@tryghost/errors');
4
- const mega = require('../../services/mega');
5
1
  const emailService = require('../../services/email-service');
6
- const labs = require('../../../shared/labs');
7
- const messages = {
8
- postNotFound: 'Post not found.'
9
- };
10
-
11
- const emailPreview = new mega.EmailPreview();
12
2
 
13
3
  module.exports = {
14
4
  docName: 'email_previews',
@@ -30,25 +20,7 @@ module.exports = {
30
20
  ],
31
21
  permissions: true,
32
22
  async query(frame) {
33
- if (labs.isSet('emailStability')) {
34
- return await emailService.controller.previewEmail(frame);
35
- }
36
-
37
- const options = Object.assign(frame.options, {formats: 'html,plaintext', withRelated: ['authors', 'posts_meta']});
38
- const data = Object.assign(frame.data, {status: 'all'});
39
-
40
- const model = await models.Post.findOne(data, options);
41
-
42
- if (!model) {
43
- throw new errors.NotFoundError({
44
- message: tpl(messages.postNotFound)
45
- });
46
- }
47
-
48
- return emailPreview.generateEmailContent(model, {
49
- newsletter: frame.options.newsletter,
50
- memberSegment: frame.options.memberSegment
51
- });
23
+ return await emailService.controller.previewEmail(frame);
52
24
  }
53
25
  },
54
26
  sendTestEmail: {
@@ -66,20 +38,7 @@ module.exports = {
66
38
  },
67
39
  permissions: true,
68
40
  async query(frame) {
69
- if (labs.isSet('emailStability')) {
70
- return await emailService.controller.sendTestEmail(frame);
71
- }
72
-
73
- const options = Object.assign(frame.options, {status: 'all'});
74
- let model = await models.Post.findOne(options, {withRelated: ['authors']});
75
-
76
- if (!model) {
77
- throw new errors.NotFoundError({
78
- message: tpl(messages.postNotFound)
79
- });
80
- }
81
- const {emails = [], memberSegment, newsletter = ''} = frame.data;
82
- return await mega.mega.sendTestEmail(model, emails, memberSegment, newsletter);
41
+ return await emailService.controller.sendTestEmail(frame);
83
42
  }
84
43
  }
85
44
  };
@@ -1,9 +1,7 @@
1
1
  const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
- const megaService = require('../../services/mega');
5
4
  const emailService = require('../../services/email-service');
6
- const labs = require('../../../shared/labs');
7
5
  const emailAnalytics = require('../../services/email-analytics');
8
6
 
9
7
  const messages = {
@@ -63,27 +61,8 @@ module.exports = {
63
61
  'id'
64
62
  ],
65
63
  permissions: true,
66
- // (complexity removed with new labs flag)
67
- // eslint-disable-next-line ghost/ghost-custom/max-api-complexity
68
64
  async query(frame) {
69
- if (labs.isSet('emailStability')) {
70
- return await emailService.controller.retryFailedEmail(frame);
71
- }
72
-
73
- const model = await models.Email.findOne(frame.data, frame.options);
74
- if (!model) {
75
- throw new errors.NotFoundError({
76
- message: tpl(messages.emailNotFound)
77
- });
78
- }
79
-
80
- if (model.get('status') !== 'failed') {
81
- throw new errors.IncorrectUsageError({
82
- message: tpl(messages.retryNotAllowed)
83
- });
84
- }
85
-
86
- return await megaService.mega.retryFailedEmail(model);
65
+ return await emailService.controller.retryFailedEmail(frame);
87
66
  }
88
67
  },
89
68
 
@@ -9,7 +9,8 @@ module.exports = {
9
9
  'limit',
10
10
  'order',
11
11
  'page',
12
- 'debug'
12
+ 'debug',
13
+ 'unique'
13
14
  ],
14
15
  permissions: true,
15
16
  query(frame) {
@@ -1,19 +1,22 @@
1
- const mega = require('../../../../../../services/mega');
2
1
  const labs = require('../../../../../../../shared/labs');
3
2
  const config = require('../../../../../../../shared/config');
3
+ const emailService = require('../../../../../../services/email-service');
4
4
 
5
5
  module.exports = (model, frame) => {
6
6
  const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
7
7
 
8
8
  // Ensure we're not outputting unwanted replacement strings when viewing email contents
9
9
  // TODO: extract this to a utility, it's duplicated in the email-preview API controller
10
- const replacements = mega.postEmailSerializer.parseReplacements(jsonModel);
11
- replacements.forEach((replacement) => {
12
- jsonModel[replacement.format] = jsonModel[replacement.format].replace(
13
- replacement.regexp,
14
- replacement.fallback || ''
15
- );
16
- });
10
+ if (jsonModel.html) {
11
+ const replacements = emailService.renderer.buildReplacementDefinitions({html: jsonModel.html, newsletterUuid: 'preview'});
12
+ const exampleMember = emailService.service.getDefaultExampleMember();
13
+
14
+ jsonModel.html = emailService.service.replaceDefinitions(jsonModel.html, replacements, exampleMember);
15
+
16
+ if (jsonModel.plaintext) {
17
+ jsonModel.plaintext = emailService.service.replaceDefinitions(jsonModel.plaintext, replacements, exampleMember);
18
+ }
19
+ }
17
20
 
18
21
  if (!labs.isSet('emailErrors') && !!(config.get('bulkEmail') && config.get('bulkEmail').mailgun)) {
19
22
  if (jsonModel.status === 'failed') {
@@ -21,5 +24,8 @@ module.exports = (model, frame) => {
21
24
  }
22
25
  }
23
26
 
27
+ // Removed loaded post relation if set
28
+ delete jsonModel.post;
29
+
24
30
  return jsonModel;
25
31
  };
@@ -12,10 +12,10 @@ module.exports = {
12
12
  }
13
13
  let posts = [];
14
14
 
15
- const tiersModels = await membersService.api.productRepository.list({
15
+ const tiersModels = await membersService.api?.productRepository.list({
16
16
  limit: 'all'
17
17
  });
18
- const tiers = tiersModels.data ? tiersModels.data.map(tierModel => tierModel.toJSON()) : [];
18
+ const tiers = tiersModels?.data ? tiersModels.data.map(tierModel => tierModel.toJSON()) : [];
19
19
  if (models.meta) {
20
20
  for (let model of models.data) {
21
21
  let post = await mappers.posts(model, frame, {tiers});
@@ -3,7 +3,6 @@
3
3
  const fs = require('fs-extra');
4
4
 
5
5
  const path = require('path');
6
- const Promise = require('bluebird');
7
6
  const config = require('../../../shared/config');
8
7
  const logging = require('@tryghost/logging');
9
8
  const urlUtils = require('../../../shared/url-utils');
@@ -32,7 +31,7 @@ const readBackup = async (filename) => {
32
31
  const exists = await fs.pathExists(backupPath);
33
32
 
34
33
  if (exists) {
35
- const backupFile = await fs.readFile(backupPath);
34
+ const backupFile = await fs.readFile(backupPath, 'utf8');
36
35
  return JSON.parse(backupFile);
37
36
  } else {
38
37
  return null;
@@ -40,23 +39,24 @@ const readBackup = async (filename) => {
40
39
  };
41
40
 
42
41
  /**
43
- * ## Backup
44
- * does an export, and stores this in a local file
45
- * @returns {Promise<*>}
42
+ * Does an export, and stores this in a local file
43
+ *
44
+ * @param {Object} options
45
+ * @returns {Promise<String>}
46
46
  */
47
47
  const backup = async function backup(options = {}) {
48
48
  logging.info('Creating database backup');
49
49
 
50
- const props = {
51
- data: exporter.doExport(options),
52
- filename: exporter.fileName(options)
53
- };
50
+ const filename = await exporter.fileName(options);
51
+ const data = await exporter.doExport(options);
54
52
 
55
- const exportResult = await Promise.props(props);
56
- const filename = await writeExportFile(exportResult);
53
+ const filePath = await writeExportFile({
54
+ data,
55
+ filename
56
+ });
57
57
 
58
- logging.info('Database backup written to: ' + filename);
59
- return filename;
58
+ logging.info(`Database backup written to ${filePath}`);
59
+ return filePath;
60
60
  };
61
61
 
62
62
  module.exports = {
@@ -15,7 +15,7 @@ ImageHandler = {
15
15
  const store = storage.getStorage('images');
16
16
  const baseDirRegex = baseDir ? new RegExp('^' + baseDir + '/') : new RegExp('');
17
17
 
18
- const imageFolderRegexes = _.map(urlUtils.STATIC_IMAGE_URL_PREFIX.split('/'), function (dir) {
18
+ const imageFolderRegexes = _.map(store.staticFileURLPrefix.split('/'), function (dir) {
19
19
  return new RegExp('^' + dir + '/');
20
20
  });
21
21
 
@@ -36,7 +36,7 @@ ImageHandler = {
36
36
 
37
37
  return Promise.all(files.map(function (image) {
38
38
  return store.getUniqueFileName(image, image.targetDir).then(function (targetFilename) {
39
- image.newPath = urlUtils.urlJoin('/', urlUtils.getSubdir(), urlUtils.STATIC_IMAGE_URL_PREFIX,
39
+ image.newPath = urlUtils.urlJoin('/', urlUtils.getSubdir(), store.staticFileURLPrefix,
40
40
  path.relative(config.getContentPath('images'), targetFilename));
41
41
 
42
42
  return image;
@@ -11,15 +11,19 @@ const debug = require('@tryghost/debug')('import-manager');
11
11
  const logging = require('@tryghost/logging');
12
12
  const errors = require('@tryghost/errors');
13
13
  const ImageHandler = require('./handlers/image');
14
+ const ImporterContentFileHandler = require('@tryghost/importer-handler-content-files');
14
15
  const RevueHandler = require('./handlers/revue');
15
16
  const JSONHandler = require('./handlers/json');
16
17
  const MarkdownHandler = require('./handlers/markdown');
17
- const ImageImporter = require('./importers/image');
18
+ const ContentFileImporter = require('./importers/ContentFileImporter');
18
19
  const RevueImporter = require('@tryghost/importer-revue');
19
20
  const DataImporter = require('./importers/data');
20
21
  const urlUtils = require('../../../shared/url-utils');
21
22
  const {GhostMailer} = require('../../services/mail');
22
23
  const jobManager = require('../../services/jobs');
24
+ const mediaStorage = require('../../adapters/storage').getStorage('media');
25
+ const imageStorage = require('../../adapters/storage').getStorage('images');
26
+ const fileStorage = require('../../adapters/storage').getStorage('files');
23
27
 
24
28
  const emailTemplate = require('./email-template');
25
29
  const ghostMailer = new GhostMailer();
@@ -51,15 +55,55 @@ let defaults = {
51
55
 
52
56
  class ImportManager {
53
57
  constructor() {
58
+ const mediaHandler = new ImporterContentFileHandler({
59
+ type: 'media',
60
+ // @NOTE: making the second parameter strict folder "content/media" brakes the glob pattern
61
+ // in the importer, so we need to keep it as general "content" unless
62
+ // it becomes a strict requirement
63
+ directories: ['media', 'content'],
64
+ extensions: config.get('uploads').media.extensions,
65
+ contentTypes: config.get('uploads').media.contentTypes,
66
+ contentPath: config.getContentPath('media'),
67
+ urlUtils: urlUtils,
68
+ storage: mediaStorage
69
+ });
70
+
71
+ const filesHandler = new ImporterContentFileHandler({
72
+ type: 'files',
73
+ // @NOTE: making the second parameter strict folder "content/files" brakes the glob pattern
74
+ // in the importer, so we need to keep it as general "content" unless
75
+ // it becomes a strict requirement
76
+ directories: ['files', 'content'],
77
+ extensions: config.get('uploads').files.extensions,
78
+ contentTypes: config.get('uploads').files.contentTypes,
79
+ contentPath: config.getContentPath('files'),
80
+ urlUtils: urlUtils,
81
+ storage: fileStorage
82
+ });
83
+
84
+ const imageImporter = new ContentFileImporter({
85
+ type: 'images',
86
+ store: imageStorage
87
+ });
88
+ const mediaImporter = new ContentFileImporter({
89
+ type: 'media',
90
+ store: mediaStorage
91
+ });
92
+
93
+ const contentFilesImporter = new ContentFileImporter({
94
+ type: 'files',
95
+ store: fileStorage
96
+ });
97
+
54
98
  /**
55
99
  * @type {Importer[]} importers
56
100
  */
57
- this.importers = [ImageImporter, RevueImporter, DataImporter];
101
+ this.importers = [imageImporter, mediaImporter, contentFilesImporter, RevueImporter, DataImporter];
58
102
 
59
103
  /**
60
104
  * @type {Handler[]}
61
105
  */
62
- this.handlers = [ImageHandler, RevueHandler, JSONHandler, MarkdownHandler];
106
+ this.handlers = [ImageHandler, mediaHandler, filesHandler, RevueHandler, JSONHandler, MarkdownHandler];
63
107
 
64
108
  // Keep track of file to cleanup at the end
65
109
  /**
@@ -257,7 +301,14 @@ class ImportManager {
257
301
  const baseDir = this.getBaseDirectory(zipDirectory);
258
302
 
259
303
  for (const handler of this.handlers) {
260
- const files = this.getFilesFromZip(handler, zipDirectory);
304
+ let files = [];
305
+ if (handler.directories?.length > 0) {
306
+ for (const dir of handler.directories) {
307
+ files.push(...this.getFilesFromZip(handler, path.join(zipDirectory, (baseDir || ''), dir)));
308
+ }
309
+ } else {
310
+ files.push(...this.getFilesFromZip(handler, zipDirectory));
311
+ }
261
312
 
262
313
  debug('handler', handler.type, files);
263
314
 
@@ -294,7 +345,15 @@ class ImportManager {
294
345
  */
295
346
  async processFile(file, ext) {
296
347
  const fileHandlers = _.filter(this.handlers, function (handler) {
297
- return _.includes(handler.extensions, ext);
348
+ let match = _.includes(handler.extensions, ext);
349
+
350
+ // CASE: content file handlers should ignore files in the root directory
351
+ if (match && handler.directories && handler.directories.length) {
352
+ const dir = path.dirname(file.path)?.split('/')[1];
353
+ match = _.includes(handler.directories, dir);
354
+ }
355
+
356
+ return match;
298
357
  });
299
358
 
300
359
  const importData = {};
@@ -0,0 +1,128 @@
1
+ const _ = require('lodash');
2
+ let replaceImage;
3
+ let preProcessPosts;
4
+ let preProcessTags;
5
+ let preProcessUsers;
6
+
7
+ replaceImage = function (markdown, image) {
8
+ if (!markdown) {
9
+ return;
10
+ }
11
+
12
+ // Normalizes to include a trailing slash if there was one
13
+ const regex = new RegExp('(/)?' + image.originalPath, 'gm');
14
+
15
+ return markdown.replace(regex, image.newPath);
16
+ };
17
+
18
+ /**
19
+ * @param {Object} data
20
+ * @param {Object[]} data.posts
21
+ * @param {Object} contentFile
22
+ * @param {String} contentFile.originalPath
23
+ * @param {String} contentFile.newPath
24
+ */
25
+ preProcessPosts = function (data, contentFile) {
26
+ _.each(data.posts, function (post) {
27
+ post.markdown = replaceImage(post.markdown, contentFile);
28
+ if (post.html) {
29
+ post.html = replaceImage(post.html, contentFile);
30
+ }
31
+ if (post.feature_image) {
32
+ post.feature_image = replaceImage(post.feature_image, contentFile);
33
+ }
34
+ });
35
+ };
36
+
37
+ preProcessTags = function (data, image) {
38
+ _.each(data.tags, function (tag) {
39
+ if (tag.feature_image) {
40
+ tag.feature_image = replaceImage(tag.feature_image, image);
41
+ }
42
+ });
43
+ };
44
+
45
+ preProcessUsers = function (data, image) {
46
+ _.each(data.users, function (user) {
47
+ if (user.cover_image) {
48
+ user.cover_image = replaceImage(user.cover_image, image);
49
+ }
50
+ if (user.profile_image) {
51
+ user.profile_image = replaceImage(user.profile_image, image);
52
+ }
53
+ });
54
+ };
55
+
56
+ class ContentFileImporter {
57
+ /** @property {string} */
58
+ type;
59
+
60
+ /** @property {import('ghost-storage-base')} */
61
+ #store;
62
+
63
+ /**
64
+ *
65
+ * @param {Object} deps
66
+ * @param {'images' | 'media' | 'files'} deps.type - importer type
67
+ * @param {import('ghost-storage-base')} deps.store
68
+ */
69
+ constructor(deps) {
70
+ this.type = deps.type;
71
+ this.#store = deps.store;
72
+ }
73
+
74
+ preProcess(importData) {
75
+ if (this.type === 'images') {
76
+ if (importData.images && importData.data && importData.data.data) {
77
+ _.each(importData.images, function (image) {
78
+ preProcessPosts(importData.data.data, image);
79
+ preProcessTags(importData.data.data, image);
80
+ preProcessUsers(importData.data.data, image);
81
+ });
82
+ }
83
+
84
+ importData.preProcessedByImage = true;
85
+ }
86
+
87
+ // @NOTE: the type === 'media' check does not belong here and should be abstracted away
88
+ // to make this importer more generic
89
+ if (this.type === 'media') {
90
+ if (importData.media && importData.data && importData.data.data) {
91
+ _.each(importData.media, function (file) {
92
+ preProcessPosts(importData.data.data, file);
93
+ });
94
+ }
95
+
96
+ importData.preProcessedByMedia = true;
97
+ }
98
+
99
+ if (this.type === 'files') {
100
+ if (importData.files && importData.data && importData.data.data) {
101
+ _.each(importData.files, function (file) {
102
+ preProcessPosts(importData.data.data, file);
103
+ });
104
+ }
105
+
106
+ importData.preProcessedByFiles = true;
107
+ }
108
+
109
+ return importData;
110
+ }
111
+
112
+ /**
113
+ *
114
+ * @param {Object[]} contentFilesData
115
+ * @returns
116
+ */
117
+ doImport(contentFilesData) {
118
+ const store = this.#store;
119
+
120
+ return Promise.all(contentFilesData.map(function (contentFile) {
121
+ return store.save(contentFile, contentFile.targetDir).then(function (result) {
122
+ return {originalPath: contentFile.originalPath, newPath: contentFile.newPath, stored: result};
123
+ });
124
+ }));
125
+ }
126
+ }
127
+
128
+ module.exports = ContentFileImporter;
@@ -4,7 +4,7 @@ const htmlToPlaintext = require('@tryghost/html-to-plaintext');
4
4
  const mobiledocLib = require('../../../../lib/mobiledoc');
5
5
  const {createTransactionalMigration} = require('../../utils');
6
6
 
7
- // in Ghost versions 4.6.1-4.8.4 the 4.0 migration that transfored URLs had a bug
7
+ // in Ghost versions 4.6.1-4.8.4 the 4.0 migration that transformed URLs had a bug
8
8
  // that meant urls inside cards in mobiledoc content was not being transformed
9
9
  //
10
10
  // if the migrations table indicates an upgrade was made from 3.x to 4.6-4.8 then