ghost 5.36.1 → 5.37.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 (177) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.37.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.37.0.tgz +0 -0
  3. package/components/tryghost-adapter-manager-5.37.0.tgz +0 -0
  4. package/components/tryghost-api-framework-5.37.0.tgz +0 -0
  5. package/components/tryghost-api-version-compatibility-service-5.37.0.tgz +0 -0
  6. package/components/tryghost-audience-feedback-5.37.0.tgz +0 -0
  7. package/components/tryghost-bootstrap-socket-5.37.0.tgz +0 -0
  8. package/components/tryghost-constants-5.37.0.tgz +0 -0
  9. package/components/{tryghost-custom-theme-settings-service-5.36.1.tgz → tryghost-custom-theme-settings-service-5.37.0.tgz} +0 -0
  10. package/components/tryghost-data-generator-5.37.0.tgz +0 -0
  11. package/components/tryghost-domain-events-5.37.0.tgz +0 -0
  12. package/components/tryghost-dynamic-routing-events-5.37.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-provider-mailgun-5.37.0.tgz +0 -0
  14. package/components/tryghost-email-analytics-service-5.37.0.tgz +0 -0
  15. package/components/tryghost-email-content-generator-5.37.0.tgz +0 -0
  16. package/components/tryghost-email-events-5.37.0.tgz +0 -0
  17. package/components/{tryghost-email-service-5.36.1.tgz → tryghost-email-service-5.37.0.tgz} +0 -0
  18. package/components/tryghost-email-suppression-list-5.37.0.tgz +0 -0
  19. package/components/tryghost-event-aware-cache-wrapper-5.37.0.tgz +0 -0
  20. package/components/{tryghost-express-dynamic-redirects-5.36.1.tgz → tryghost-express-dynamic-redirects-5.37.0.tgz} +0 -0
  21. package/components/tryghost-external-media-inliner-5.37.0.tgz +0 -0
  22. package/components/tryghost-extract-api-key-5.37.0.tgz +0 -0
  23. package/components/tryghost-html-to-plaintext-5.37.0.tgz +0 -0
  24. package/components/tryghost-i18n-5.37.0.tgz +0 -0
  25. package/components/tryghost-importer-handler-content-files-5.37.0.tgz +0 -0
  26. package/components/tryghost-importer-revue-5.37.0.tgz +0 -0
  27. package/components/{tryghost-job-manager-5.36.1.tgz → tryghost-job-manager-5.37.0.tgz} +0 -0
  28. package/components/tryghost-link-redirects-5.37.0.tgz +0 -0
  29. package/components/tryghost-link-replacer-5.37.0.tgz +0 -0
  30. package/components/tryghost-link-tracking-5.37.0.tgz +0 -0
  31. package/components/tryghost-magic-link-5.37.0.tgz +0 -0
  32. package/components/tryghost-mailgun-client-5.37.0.tgz +0 -0
  33. package/components/{tryghost-member-attribution-5.36.1.tgz → tryghost-member-attribution-5.37.0.tgz} +0 -0
  34. package/components/tryghost-member-events-5.37.0.tgz +0 -0
  35. package/components/tryghost-members-api-5.37.0.tgz +0 -0
  36. package/components/tryghost-members-csv-5.37.0.tgz +0 -0
  37. package/components/{tryghost-members-events-service-5.36.1.tgz → tryghost-members-events-service-5.37.0.tgz} +0 -0
  38. package/components/tryghost-members-importer-5.37.0.tgz +0 -0
  39. package/components/{tryghost-members-offers-5.36.1.tgz → tryghost-members-offers-5.37.0.tgz} +0 -0
  40. package/components/tryghost-members-payments-5.37.0.tgz +0 -0
  41. package/components/{tryghost-members-ssr-5.36.1.tgz → tryghost-members-ssr-5.37.0.tgz} +0 -0
  42. package/components/tryghost-members-stripe-service-5.37.0.tgz +0 -0
  43. package/components/tryghost-milestones-5.37.0.tgz +0 -0
  44. package/components/tryghost-minifier-5.37.0.tgz +0 -0
  45. package/components/tryghost-mw-api-version-mismatch-5.37.0.tgz +0 -0
  46. package/components/tryghost-mw-cache-control-5.37.0.tgz +0 -0
  47. package/components/tryghost-mw-error-handler-5.37.0.tgz +0 -0
  48. package/components/tryghost-mw-session-from-token-5.37.0.tgz +0 -0
  49. package/components/tryghost-mw-update-user-last-seen-5.37.0.tgz +0 -0
  50. package/components/tryghost-mw-version-match-5.37.0.tgz +0 -0
  51. package/components/tryghost-mw-vhost-5.37.0.tgz +0 -0
  52. package/components/tryghost-oembed-service-5.37.0.tgz +0 -0
  53. package/components/tryghost-package-json-5.37.0.tgz +0 -0
  54. package/components/tryghost-referrers-5.37.0.tgz +0 -0
  55. package/components/tryghost-security-5.37.0.tgz +0 -0
  56. package/components/tryghost-session-service-5.37.0.tgz +0 -0
  57. package/components/tryghost-settings-path-manager-5.37.0.tgz +0 -0
  58. package/components/tryghost-slack-notifications-5.37.0.tgz +0 -0
  59. package/components/{tryghost-staff-service-5.36.1.tgz → tryghost-staff-service-5.37.0.tgz} +0 -0
  60. package/components/tryghost-stats-service-5.37.0.tgz +0 -0
  61. package/components/tryghost-tiers-5.37.0.tgz +0 -0
  62. package/components/tryghost-update-check-service-5.37.0.tgz +0 -0
  63. package/components/tryghost-verification-trigger-5.37.0.tgz +0 -0
  64. package/components/tryghost-version-notifications-data-service-5.37.0.tgz +0 -0
  65. package/components/tryghost-webmentions-5.37.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 +4 -2
  73. package/core/built/admin/assets/chunk.143.27cd10a38f877e715b35.js +49 -0
  74. package/core/built/admin/assets/chunk.178.dd6cf17fb0986acf19d6.js +10 -0
  75. package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js → chunk.652.bb618bc5abf23bed4e87.js} +855 -784
  76. package/core/built/admin/assets/{chunk.79.53e8aa9671b2d5dae8ba.js → chunk.79.4a959c324df25480b90e.js} +191 -184
  77. package/core/built/admin/assets/{ghost-b828e9e3c161aae92909c2e163656bb1.js → ghost-2948791640be026b987b88f89034bc85.js} +260 -238
  78. package/core/built/admin/assets/ghost-dark-6ea4b338f17a43c204b7c1e207b90cd7.css +1 -0
  79. package/core/built/admin/assets/ghost-efbe4dcc249d119a955b038aae5c980d.css +1 -0
  80. package/core/built/admin/assets/{vendor-c4684647d4f5213e5dbb6763de430e7e.js → vendor-b982e3bf1020bff77b2a3c44d5f59e55.js} +176 -191
  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/meta/asset-url.js +9 -0
  84. package/core/server/api/endpoints/db.js +17 -0
  85. package/core/server/api/endpoints/mentions.js +2 -1
  86. package/core/server/api/endpoints/utils/serializers/output/posts.js +2 -2
  87. package/core/server/data/db/backup.js +13 -13
  88. package/core/server/data/importer/handlers/image.js +2 -2
  89. package/core/server/data/importer/import-manager.js +56 -4
  90. package/core/server/data/importer/importers/ContentFileImporter.js +128 -0
  91. package/core/server/data/schema/commands.js +21 -10
  92. package/core/server/models/mention.js +13 -0
  93. package/core/server/run-update-check.js +3 -1
  94. package/core/server/services/media-inliner/index.js +1 -0
  95. package/core/server/services/media-inliner/service.js +16 -0
  96. package/core/server/services/members/stats/members-stats.js +13 -9
  97. package/core/server/services/mentions/BookshelfMentionRepository.js +12 -1
  98. package/core/server/services/mentions/MentionController.js +7 -1
  99. package/core/server/services/mentions/WebmentionMetadata.js +3 -2
  100. package/core/server/services/mentions/service.js +1 -4
  101. package/core/server/{analytics-events.js → services/segment/index.js} +4 -3
  102. package/core/server/services/stripe/config.js +4 -0
  103. package/core/server/update-check.js +5 -3
  104. package/core/server/web/api/endpoints/admin/app.js +5 -4
  105. package/core/server/web/api/endpoints/admin/routes.js +6 -0
  106. package/core/server/web/api/middleware/index.js +1 -2
  107. package/core/shared/config/overrides.json +34 -0
  108. package/core/shared/labs.js +3 -2
  109. package/package.json +153 -148
  110. package/yarn.lock +765 -820
  111. package/components/tryghost-adapter-cache-memory-ttl-5.36.1.tgz +0 -0
  112. package/components/tryghost-adapter-cache-redis-5.36.1.tgz +0 -0
  113. package/components/tryghost-adapter-manager-5.36.1.tgz +0 -0
  114. package/components/tryghost-api-framework-5.36.1.tgz +0 -0
  115. package/components/tryghost-api-version-compatibility-service-5.36.1.tgz +0 -0
  116. package/components/tryghost-audience-feedback-5.36.1.tgz +0 -0
  117. package/components/tryghost-bootstrap-socket-5.36.1.tgz +0 -0
  118. package/components/tryghost-constants-5.36.1.tgz +0 -0
  119. package/components/tryghost-data-generator-5.36.1.tgz +0 -0
  120. package/components/tryghost-domain-events-5.36.1.tgz +0 -0
  121. package/components/tryghost-dynamic-routing-events-5.36.1.tgz +0 -0
  122. package/components/tryghost-email-analytics-provider-mailgun-5.36.1.tgz +0 -0
  123. package/components/tryghost-email-analytics-service-5.36.1.tgz +0 -0
  124. package/components/tryghost-email-content-generator-5.36.1.tgz +0 -0
  125. package/components/tryghost-email-events-5.36.1.tgz +0 -0
  126. package/components/tryghost-email-suppression-list-5.36.1.tgz +0 -0
  127. package/components/tryghost-event-aware-cache-wrapper-5.36.1.tgz +0 -0
  128. package/components/tryghost-extract-api-key-5.36.1.tgz +0 -0
  129. package/components/tryghost-html-to-plaintext-5.36.1.tgz +0 -0
  130. package/components/tryghost-i18n-5.36.1.tgz +0 -0
  131. package/components/tryghost-importer-revue-5.36.1.tgz +0 -0
  132. package/components/tryghost-link-redirects-5.36.1.tgz +0 -0
  133. package/components/tryghost-link-replacer-5.36.1.tgz +0 -0
  134. package/components/tryghost-link-tracking-5.36.1.tgz +0 -0
  135. package/components/tryghost-magic-link-5.36.1.tgz +0 -0
  136. package/components/tryghost-mailgun-client-5.36.1.tgz +0 -0
  137. package/components/tryghost-member-events-5.36.1.tgz +0 -0
  138. package/components/tryghost-members-api-5.36.1.tgz +0 -0
  139. package/components/tryghost-members-csv-5.36.1.tgz +0 -0
  140. package/components/tryghost-members-importer-5.36.1.tgz +0 -0
  141. package/components/tryghost-members-payments-5.36.1.tgz +0 -0
  142. package/components/tryghost-members-stripe-service-5.36.1.tgz +0 -0
  143. package/components/tryghost-milestones-5.36.1.tgz +0 -0
  144. package/components/tryghost-minifier-5.36.1.tgz +0 -0
  145. package/components/tryghost-mw-api-version-mismatch-5.36.1.tgz +0 -0
  146. package/components/tryghost-mw-cache-control-5.36.1.tgz +0 -0
  147. package/components/tryghost-mw-error-handler-5.36.1.tgz +0 -0
  148. package/components/tryghost-mw-session-from-token-5.36.1.tgz +0 -0
  149. package/components/tryghost-mw-update-user-last-seen-5.36.1.tgz +0 -0
  150. package/components/tryghost-mw-vhost-5.36.1.tgz +0 -0
  151. package/components/tryghost-oembed-service-5.36.1.tgz +0 -0
  152. package/components/tryghost-package-json-5.36.1.tgz +0 -0
  153. package/components/tryghost-referrers-5.36.1.tgz +0 -0
  154. package/components/tryghost-security-5.36.1.tgz +0 -0
  155. package/components/tryghost-session-service-5.36.1.tgz +0 -0
  156. package/components/tryghost-settings-path-manager-5.36.1.tgz +0 -0
  157. package/components/tryghost-slack-notifications-5.36.1.tgz +0 -0
  158. package/components/tryghost-stats-service-5.36.1.tgz +0 -0
  159. package/components/tryghost-tiers-5.36.1.tgz +0 -0
  160. package/components/tryghost-update-check-service-5.36.1.tgz +0 -0
  161. package/components/tryghost-verification-trigger-5.36.1.tgz +0 -0
  162. package/components/tryghost-version-notifications-data-service-5.36.1.tgz +0 -0
  163. package/components/tryghost-webmentions-5.36.1.tgz +0 -0
  164. package/core/built/admin/assets/chunk.143.26ea9f26571d656653f0.js +0 -49
  165. package/core/built/admin/assets/chunk.178.dd71b3a764b73facc400.js +0 -11
  166. package/core/built/admin/assets/ghost-7ecf5c7934d90798485ee5ac2956f7fe.css +0 -1
  167. package/core/built/admin/assets/ghost-dark-e50717df8e57d3e7fee67a0bcea895ad.css +0 -1
  168. package/core/frontend/src/cards/css/before-after.css +0 -81
  169. package/core/frontend/src/cards/js/before-after.js +0 -36
  170. package/core/server/data/importer/importers/image.js +0 -76
  171. package/core/server/data/schema/clients/index.js +0 -7
  172. package/core/server/data/schema/clients/mysql.js +0 -34
  173. package/core/server/data/schema/clients/sqlite3.js +0 -39
  174. package/core/server/services/mentions/WebmentionRequest.js +0 -20
  175. package/core/server/web/admin/middleware.js +0 -17
  176. package/core/server/web/api/middleware/version-match.js +0 -31
  177. /package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js.LICENSE.txt → chunk.652.bb618bc5abf23bed4e87.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.37%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-efbe4dcc249d119a955b038aae5c980d.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.652.bb618bc5abf23bed4e87.js"></script>
61
+ <script src="assets/chunk.143.27cd10a38f877e715b35.js"></script>
62
+ <script src="assets/ghost-2948791640be026b987b88f89034bc85.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.
@@ -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.
@@ -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
@@ -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) {
@@ -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
  /**
@@ -294,7 +338,15 @@ class ImportManager {
294
338
  */
295
339
  async processFile(file, ext) {
296
340
  const fileHandlers = _.filter(this.handlers, function (handler) {
297
- return _.includes(handler.extensions, ext);
341
+ let match = _.includes(handler.extensions, ext);
342
+
343
+ // CASE: content file handlers should ignore files in the root directory
344
+ if (match && handler.directories && handler.directories.length) {
345
+ const dir = path.dirname(file.path)?.split('/')[1];
346
+ match = _.includes(handler.directories, dir);
347
+ }
348
+
349
+ return match;
298
350
  });
299
351
 
300
352
  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;
@@ -6,7 +6,6 @@ const tpl = require('@tryghost/tpl');
6
6
  const db = require('../db');
7
7
  const DatabaseInfo = require('@tryghost/database-info');
8
8
  const schema = require('./schema');
9
- const clients = require('./clients');
10
9
 
11
10
  const messages = {
12
11
  hasPrimaryKeySQLiteError: 'Must use hasPrimaryKeySQLite on an SQLite3 database',
@@ -432,11 +431,15 @@ function deleteTable(table, transaction = db.knex) {
432
431
  /**
433
432
  * @param {import('knex').Knex} [transaction] - connection to the DB
434
433
  */
435
- function getTables(transaction = db.knex) {
434
+ async function getTables(transaction = db.knex) {
436
435
  const client = transaction.client.config.client;
437
436
 
438
- if (_.includes(_.keys(clients), client)) {
439
- return clients[client].getTables(transaction);
437
+ if (client === 'sqlite3') {
438
+ const response = await transaction.raw('select * from sqlite_master where type = "table"');
439
+ return _.reject(_.map(response, 'tbl_name'), name => name === 'sqlite_sequence');
440
+ } else if (client === 'mysql2') {
441
+ const response = await transaction.raw('show tables');
442
+ return _.flatten(_.map(response[0], entry => _.values(entry)));
440
443
  }
441
444
 
442
445
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
@@ -446,11 +449,15 @@ function getTables(transaction = db.knex) {
446
449
  * @param {string} table
447
450
  * @param {import('knex').Knex} [transaction] - connection to the DB
448
451
  */
449
- function getIndexes(table, transaction = db.knex) {
452
+ async function getIndexes(table, transaction = db.knex) {
450
453
  const client = transaction.client.config.client;
451
454
 
452
- if (_.includes(_.keys(clients), client)) {
453
- return clients[client].getIndexes(table, transaction);
455
+ if (client === 'sqlite3') {
456
+ const response = await transaction.raw(`pragma index_list("${table}")`);
457
+ return _.flatten(_.map(response, 'name'));
458
+ } else if (client === 'mysql2') {
459
+ const response = await transaction.raw(`SHOW INDEXES from ${table}`);
460
+ return _.flatten(_.map(response[0], 'Key_name'));
454
461
  }
455
462
 
456
463
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
@@ -460,11 +467,15 @@ function getIndexes(table, transaction = db.knex) {
460
467
  * @param {string} table
461
468
  * @param {import('knex').Knex} [transaction] - connection to the DB
462
469
  */
463
- function getColumns(table, transaction = db.knex) {
470
+ async function getColumns(table, transaction = db.knex) {
464
471
  const client = transaction.client.config.client;
465
472
 
466
- if (_.includes(_.keys(clients), client)) {
467
- return clients[client].getColumns(table);
473
+ if (client === 'sqlite3') {
474
+ const response = await transaction.raw(`pragma table_info("${table}")`);
475
+ return _.flatten(_.map(response, 'name'));
476
+ } else if (client === 'mysql2') {
477
+ const response = await transaction.raw(`SHOW COLUMNS from ${table}`);
478
+ return _.flatten(_.map(response[0], 'Field'));
468
479
  }
469
480
 
470
481
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
@@ -9,6 +9,19 @@ const Mention = ghostBookshelf.Model.extend({
9
9
  enforcedFilters() {
10
10
  return 'deleted:false';
11
11
  }
12
+ }, {
13
+ permittedOptions(methodName) {
14
+ let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
15
+ const validOptions = {
16
+ findPage: ['selectRaw', 'whereRaw']
17
+ };
18
+
19
+ if (validOptions[methodName]) {
20
+ options = options.concat(validOptions[methodName]);
21
+ }
22
+
23
+ return options;
24
+ }
12
25
  });
13
26
 
14
27
  module.exports = {
@@ -42,7 +42,9 @@ if (parentPort) {
42
42
  await settings.init();
43
43
  // Finished INIT
44
44
 
45
- await updateCheck();
45
+ await updateCheck({
46
+ rethrowErrors: true
47
+ });
46
48
 
47
49
  postParentPortMessage(`Ran update check`);
48
50
 
@@ -0,0 +1 @@
1
+ module.exports = require('./service');
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ async init() {
3
+ const debug = require('@tryghost/debug')('mediaInliner');
4
+
5
+ this.api = {
6
+ // @NOTE: the inlining should become an offloaded job
7
+ // startMediaInliner: mediaInliner.inlineMedia
8
+ startMediaInliner: (domains) => {
9
+ debug('[Inliner] Starting media inlining job for domains: ', domains);
10
+ return {
11
+ status: 'success'
12
+ };
13
+ }
14
+ };
15
+ }
16
+ };
@@ -1,5 +1,4 @@
1
1
  const moment = require('moment-timezone');
2
- const Promise = require('bluebird');
3
2
 
4
3
  const dateFormat = 'YYYY-MM-DD HH:mm:ss';
5
4
  class MembersStats {
@@ -131,14 +130,19 @@ class MembersStats {
131
130
  const totalMembers = await this.getTotalMembers();
132
131
 
133
132
  // perform final calculations in parallel
134
- const results = await Promise.props({
135
- total: totalMembers,
136
- total_in_range: this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
137
- total_on_date: this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
138
- new_today: this.getNewMembersToday({siteTimezone})
139
- });
140
-
141
- return results;
133
+ const [total, totalInRange, totalOnDate, newToday] = await Promise.all([
134
+ totalMembers,
135
+ this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
136
+ this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
137
+ this.getNewMembersToday({siteTimezone})
138
+ ]);
139
+
140
+ return {
141
+ total,
142
+ total_in_range: totalInRange,
143
+ total_on_date: totalOnDate,
144
+ new_today: newToday
145
+ };
142
146
  }
143
147
  }
144
148
 
@@ -65,7 +65,18 @@ module.exports = class BookshelfMentionRepository {
65
65
  * @returns {Promise<Page<import('@tryghost/webmentions/lib/Mention')>>}
66
66
  */
67
67
  async getPage(options) {
68
- const page = await this.#MentionModel.findPage(options);
68
+ /**
69
+ * @type {GetPageOptions & {whereRaw?: string}}
70
+ */
71
+ const _options = {
72
+ ...options
73
+ };
74
+ delete _options.unique;
75
+ if (options.unique) {
76
+ _options.whereRaw = 'NOT EXISTS (select id from mentions as m where m.id > mentions.id and m.source = mentions.source)';
77
+ }
78
+
79
+ const page = await this.#MentionModel.findPage(_options);
69
80
 
70
81
  return {
71
82
  data: await Promise.all(page.data.map(model => this.#modelToMention(model))),
@@ -80,11 +80,17 @@ module.exports = class MentionController {
80
80
  order = 'created_at asc';
81
81
  }
82
82
 
83
+ let unique;
84
+ if (frame.options.unique && (frame.options.unique === 'true' || frame.options.unique === true)) {
85
+ unique = true;
86
+ }
87
+
83
88
  const mentions = await this.#api.listMentions({
84
89
  filter: frame.options.filter,
85
90
  order,
86
91
  limit,
87
- page
92
+ page,
93
+ unique
88
94
  });
89
95
 
90
96
  const resources = await Promise.all(mentions.data.map((mention) => {
@@ -6,14 +6,15 @@ module.exports = class WebmentionMetadata {
6
6
  * @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').WebmentionMetadata>}
7
7
  */
8
8
  async fetch(url) {
9
- const data = await oembedService.fetchOembedDataFromUrl(url.href, 'bookmark');
9
+ const data = await oembedService.fetchOembedDataFromUrl(url.href, 'mention');
10
10
  const result = {
11
11
  siteTitle: data.metadata.publisher,
12
12
  title: data.metadata.title,
13
13
  excerpt: data.metadata.description,
14
14
  author: data.metadata.author,
15
15
  image: data.metadata.thumbnail ? new URL(data.metadata.thumbnail) : null,
16
- favicon: data.metadata.icon ? new URL(data.metadata.icon) : null
16
+ favicon: data.metadata.icon ? new URL(data.metadata.icon) : null,
17
+ body: data.body
17
18
  };
18
19
  return result;
19
20
  }