ghost 5.115.1 → 5.116.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/components/{tryghost-api-framework-5.115.1.tgz → tryghost-api-framework-5.116.1.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.1.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.1.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.1.tgz → tryghost-custom-theme-settings-service-5.116.1.tgz} +0 -0
  5. package/components/{tryghost-domain-events-5.115.1.tgz → tryghost-domain-events-5.116.1.tgz} +0 -0
  6. package/components/{tryghost-donations-5.115.1.tgz → tryghost-donations-5.116.1.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.1.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.1.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.1.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.1.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.1.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.1.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.1.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.1.tgz +0 -0
  15. package/components/{tryghost-member-attribution-5.115.1.tgz → tryghost-member-attribution-5.116.1.tgz} +0 -0
  16. package/components/tryghost-member-events-5.116.1.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.1.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.1.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.1.tgz → tryghost-members-offers-5.116.1.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.1.tgz → tryghost-milestones-5.116.1.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.1.tgz → tryghost-mw-error-handler-5.116.1.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.1.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.1.tgz → tryghost-post-events-5.116.1.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.1.tgz → tryghost-post-revisions-5.116.1.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.1.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.1.tgz → tryghost-prometheus-metrics-5.116.1.tgz} +0 -0
  27. package/components/tryghost-security-5.116.1.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.1.tgz → tryghost-tiers-5.116.1.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.1.tgz +0 -0
  30. package/content/themes/casper/LICENSE +1 -1
  31. package/content/themes/casper/README.md +1 -1
  32. package/content/themes/source/LICENSE +1 -1
  33. package/content/themes/source/README.md +1 -1
  34. package/content/themes/source/assets/built/screen.css +1 -1
  35. package/content/themes/source/assets/built/screen.css.map +1 -1
  36. package/content/themes/source/assets/css/screen.css +6 -11
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +0 -42
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +24764 -24129
  40. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  41. package/core/built/admin/assets/admin-x-demo/{index-15df2af5.mjs → index-a9601514.mjs} +3 -3
  42. package/core/built/admin/assets/admin-x-demo/{modals-8ca61d78.mjs → modals-c1789d04.mjs} +2 -2
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-d2e6872f.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
  44. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  45. package/core/built/admin/assets/admin-x-settings/{index-8e8821e5.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-f5cb3db3.mjs → index-f744cab7.mjs} +49 -35
  47. package/core/built/admin/assets/admin-x-settings/{modals-e8ae4d46.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.cb72a86e19c9ffd6172e.js +35 -0
  49. package/core/built/admin/assets/chunk.582.4f4d38ffe79fbdbd26f7.js +37 -0
  50. package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
  51. package/core/built/admin/assets/{ghost-df7b9558260aa27d18b195ee895b487d.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +144 -145
  52. package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
  54. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  55. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
  56. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
  57. package/core/built/admin/assets/posts/posts.js +5732 -5667
  58. package/core/built/admin/assets/stats/stats.js +71082 -7533
  59. package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
  60. package/core/built/admin/index.html +6 -6
  61. package/core/cli/generate-data.js +1 -1
  62. package/core/frontend/helpers/ghost_head.js +6 -1
  63. package/core/frontend/public/ghost-stats.js +55 -2
  64. package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
  65. package/core/frontend/services/assets-minification/CardAssets.js +1 -1
  66. package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
  67. package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
  68. package/core/frontend/services/assets-minification/Minifier.js +191 -0
  69. package/core/frontend/services/routing/controllers/previews.js +2 -1
  70. package/core/server/adapters/cache/Redis.js +1 -1
  71. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  72. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  73. package/core/server/api/endpoints/posts.js +9 -3
  74. package/core/server/api/endpoints/previews.js +35 -1
  75. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  76. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  77. package/core/server/data/db/connection.js +2 -0
  78. package/core/server/data/db/index.js +1 -0
  79. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  80. package/core/server/data/importer/import-manager.js +1 -1
  81. package/core/server/data/seeders/DataGenerator.js +288 -0
  82. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  83. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  84. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  85. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  86. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  87. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  88. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  89. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  90. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  91. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  92. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  93. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  94. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  96. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  97. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  98. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  99. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  100. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  101. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  102. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  103. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  104. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  105. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  106. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  107. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  108. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  109. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  110. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  111. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  112. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  113. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  114. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  115. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  116. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  117. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  118. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  119. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  120. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  121. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  122. package/core/server/data/seeders/importers/index.js +41 -0
  123. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  124. package/core/server/data/seeders/utils/blog-info.js +3 -0
  125. package/core/server/data/seeders/utils/database-date.js +7 -0
  126. package/core/server/data/seeders/utils/event-generator.js +48 -0
  127. package/core/server/data/seeders/utils/random.js +13 -0
  128. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  129. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  130. package/core/server/services/adapter-manager/index.js +1 -1
  131. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  132. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  133. package/core/server/services/announcement-bar-service/index.js +1 -1
  134. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +1 -1
  135. package/core/server/services/auth/session/session-service.js +15 -5
  136. package/core/server/services/custom-redirects/index.js +1 -1
  137. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +1 -1
  138. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  139. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  140. package/core/server/services/email-suppression-list/service.js +1 -1
  141. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  142. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  143. package/core/server/services/lib/InMemoryRepository.js +62 -0
  144. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  145. package/core/server/services/lib/MailgunClient.js +364 -0
  146. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  147. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  148. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  149. package/core/server/services/link-redirection/README.md +151 -0
  150. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  151. package/core/server/services/link-redirection/index.js +1 -1
  152. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  153. package/core/server/services/mail/index.js +1 -1
  154. package/core/server/services/mail-events/InMemoryMailEventRepository.js +2 -2
  155. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +1 -1
  156. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  157. package/core/server/services/offers/service.js +1 -1
  158. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  159. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  160. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  161. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  162. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  163. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  164. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  165. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  166. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  167. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  168. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  169. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  170. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  171. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  172. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  173. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  174. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  175. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  176. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  177. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  178. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  179. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  180. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  181. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  182. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  183. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  184. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  185. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  186. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  187. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  188. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  189. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  190. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  191. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  192. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  193. package/core/server/services/recommendations/service/index.js +31 -0
  194. package/core/server/services/recommendations/service/index.ts +15 -0
  195. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  196. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  197. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  198. package/core/server/services/slack-notifications/service.js +4 -6
  199. package/core/server/web/api/endpoints/admin/app.js +1 -21
  200. package/core/server/web/api/middleware/version-match.js +41 -0
  201. package/core/shared/labs.js +2 -2
  202. package/package.json +87 -104
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/yarn.lock +1470 -1540
  205. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  206. package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
  207. package/components/tryghost-announcement-bar-settings-5.115.1.tgz +0 -0
  208. package/components/tryghost-constants-5.115.1.tgz +0 -0
  209. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  210. package/components/tryghost-data-generator-5.115.1.tgz +0 -0
  211. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  212. package/components/tryghost-email-content-generator-5.115.1.tgz +0 -0
  213. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  214. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  215. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  216. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  217. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  218. package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
  219. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  220. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  221. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  222. package/components/tryghost-job-manager-5.115.1.tgz +0 -0
  223. package/components/tryghost-link-redirects-5.115.1.tgz +0 -0
  224. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  225. package/components/tryghost-magic-link-5.115.1.tgz +0 -0
  226. package/components/tryghost-mailgun-client-5.115.1.tgz +0 -0
  227. package/components/tryghost-member-events-5.115.1.tgz +0 -0
  228. package/components/tryghost-members-api-5.115.1.tgz +0 -0
  229. package/components/tryghost-members-csv-5.115.1.tgz +0 -0
  230. package/components/tryghost-members-payments-5.115.1.tgz +0 -0
  231. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  232. package/components/tryghost-mw-version-match-5.115.1.tgz +0 -0
  233. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  234. package/components/tryghost-posts-service-5.115.1.tgz +0 -0
  235. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  236. package/components/tryghost-security-5.115.1.tgz +0 -0
  237. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  238. package/components/tryghost-webmentions-5.115.1.tgz +0 -0
  239. package/core/built/admin/assets/chunk.524.2439684964c164c598ab.js +0 -35
  240. package/core/built/admin/assets/chunk.582.bf5a2bbb2c4eb69ef1e7.js +0 -37
  241. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +0 -1
  242. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +0 -1
  243. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,90 @@
1
+ const _ = require('lodash');
2
+ const path = require('path');
3
+
4
+ class ImporterContentFileHandler {
5
+ /** @property {'media' | 'files' | 'images'} */
6
+ type;
7
+
8
+ /** @property {string[]} */
9
+ directories;
10
+
11
+ /** @property {string[]} */
12
+ extensions;
13
+
14
+ /** @property {string[]} */
15
+ contentTypes;
16
+
17
+ /**
18
+ * Holds path to the destination content directory
19
+ * @property {string} */
20
+ #contentPath;
21
+
22
+ /**
23
+ *
24
+ * @param {Object} deps dependencies
25
+ * @param {'media' | 'files' | 'images'} deps.type type of content file
26
+ * @param {string[]} deps.extensions file extensions to search for
27
+ * @param {boolean} [deps.ignoreRootFolderFiles] whether to ignore files in the root folder
28
+ * @param {string[]} deps.contentTypes content types to search for
29
+ * @param {string[]} deps.directories directories to search for content files
30
+ * @param {string} deps.contentPath path to the destination content directory
31
+ * @param {Object} deps.storage storage adapter instance
32
+ * @param {object} deps.urlUtils urlUtils instance
33
+ */
34
+ constructor(deps) {
35
+ this.type = deps.type;
36
+ this.directories = deps.directories;
37
+ this.extensions = deps.extensions;
38
+ this.contentTypes = deps.contentTypes;
39
+ this.ignoreRootFolderFiles = deps.ignoreRootFolderFiles;
40
+ this.storage = deps.storage;
41
+ this.#contentPath = deps.contentPath;
42
+ this.urlUtils = deps.urlUtils;
43
+ }
44
+
45
+ async loadFile(files, baseDir) {
46
+ const baseDirRegex = baseDir ? new RegExp('^' + baseDir + '/') : new RegExp('');
47
+
48
+ const contentFilesFolderRegexes = _.map(this.storage.staticFileURLPrefix.split('/'), function (dir) {
49
+ return new RegExp('^' + dir + '/');
50
+ });
51
+
52
+ if (this.ignoreRootFolderFiles) {
53
+ files = _.filter(files, function (file) {
54
+ return file.name.indexOf('/') !== -1;
55
+ });
56
+ }
57
+
58
+ // normalize the directory structure
59
+ const filesContentPath = this.#contentPath;
60
+ files = _.map(files, function (file) {
61
+ const noBaseDir = file.name.replace(baseDirRegex, '');
62
+ let noGhostDirs = noBaseDir;
63
+
64
+ _.each(contentFilesFolderRegexes, function (regex) {
65
+ noGhostDirs = noGhostDirs.replace(regex, '');
66
+ });
67
+
68
+ file.originalPath = noBaseDir;
69
+ file.name = noGhostDirs;
70
+ file.targetDir = path.join(filesContentPath, path.dirname(noGhostDirs));
71
+ return file;
72
+ });
73
+
74
+ const self = this;
75
+ return Promise.all(files.map(function (contentFile) {
76
+ return self.storage.getUniqueFileName(contentFile, contentFile.targetDir).then(function (targetFilename) {
77
+ contentFile.newPath = self.urlUtils.urlJoin(
78
+ '/',
79
+ self.urlUtils.getSubdir(),
80
+ self.storage.staticFileURLPrefix,
81
+ path.relative(filesContentPath, targetFilename)
82
+ );
83
+
84
+ return contentFile;
85
+ });
86
+ }));
87
+ }
88
+ }
89
+
90
+ module.exports = ImporterContentFileHandler;
@@ -11,7 +11,7 @@ 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
+ const ImporterContentFileHandler = require('./handlers/ImporterContentFileHandler');
15
15
  const RevueHandler = require('./handlers/revue');
16
16
  const JSONHandler = require('./handlers/json');
17
17
  const MarkdownHandler = require('./handlers/markdown');
@@ -0,0 +1,288 @@
1
+ const path = require('path');
2
+ const fs = require('fs/promises');
3
+ const JsonImporter = require('./utils/JsonImporter');
4
+ const {getProcessRoot} = require('@tryghost/root-utils');
5
+ const topologicalSort = require('./utils/topological-sort');
6
+ const {faker} = require('@faker-js/faker');
7
+ const {faker: americanFaker} = require('@faker-js/faker/locale/en_US');
8
+ const crypto = require('crypto');
9
+ const {Buffer} = require('node:buffer');
10
+ const DatabaseInfo = require('@tryghost/database-info');
11
+ const errors = require('@tryghost/errors');
12
+ const importers = require('./importers').reduce((acc, val) => {
13
+ acc[val.table] = val;
14
+ return acc;
15
+ }, {});
16
+
17
+ class DataGenerator {
18
+ /**
19
+ *
20
+ * @param {object} options
21
+ * @param {Record<string,number>} [options.quantities] Pass in custom amounts for specific tables
22
+ * @param {number} [options.seed] If you pass the same seed, the same data will be generated if you used the same options too and if the data generation code remained the same.
23
+ */
24
+ constructor({
25
+ knex,
26
+ tables,
27
+ schemaTables,
28
+ clearDatabase = false,
29
+ baseDataPack = '',
30
+ baseUrl,
31
+ logger,
32
+ printDependencies,
33
+ withDefault,
34
+ seed,
35
+ quantities = {},
36
+ useTransaction = true
37
+ }) {
38
+ this.knex = knex;
39
+ this.tableList = tables || [];
40
+ this.schemaTables = schemaTables;
41
+ this.willClearData = clearDatabase;
42
+ this.useBaseDataPack = baseDataPack !== '';
43
+ this.baseDataPack = baseDataPack;
44
+ this.baseUrl = baseUrl;
45
+ this.logger = logger;
46
+ this.withDefault = withDefault;
47
+ this.printDependencies = printDependencies;
48
+ this.seed = seed;
49
+ this.quantities = quantities;
50
+ this.useTransaction = useTransaction;
51
+ }
52
+
53
+ sortTableList() {
54
+ // Add missing dependencies
55
+ for (const table of this.tableList) {
56
+ table.importer = importers[table.name];
57
+
58
+ // eslint-disable-next-line no-unused-vars
59
+ table.dependencies = Object.entries(this.schemaTables[table.name]).reduce((acc, [_col, data]) => {
60
+ if (data.references) {
61
+ const referencedTable = data.references.split('.')[0];
62
+ // The ghost_subscriptions_id property has a foreign key to the subscriptions table, but we don't use that table yet atm, so don't add it as a dependency
63
+ if (!acc.includes(referencedTable) && referencedTable !== 'subscriptions') {
64
+ acc.push(referencedTable);
65
+ }
66
+ }
67
+ return acc;
68
+ }, table.importer.dependencies);
69
+
70
+ for (const dependency of table.dependencies) {
71
+ if (!this.tableList.find(t => t.name === dependency)) {
72
+ this.tableList.push({
73
+ name: dependency,
74
+ importer: importers[dependency]
75
+ });
76
+ }
77
+ }
78
+ }
79
+
80
+ // Order to ensure dependencies are created before dependants
81
+ this.tableList = topologicalSort(this.tableList);
82
+ }
83
+
84
+ /**
85
+ * TODO: This needs to reverse through all dependency chains to clear data from all tables
86
+ * @param {import('knex/types').Knex.Transaction} transaction
87
+ */
88
+ async clearData(transaction) {
89
+ const tables = this.tableList.map(t => t.name).reverse();
90
+
91
+ // TODO: Remove this once we import posts_meta
92
+ tables.unshift('posts_meta');
93
+
94
+ // Clear data from any tables that are being imported
95
+ for (const table of tables) {
96
+ this.logger.debug(`Clearing table ${table}`);
97
+
98
+ if (table === 'roles_users') {
99
+ await transaction(table).del().whereNot('user_id', '1');
100
+ } else if (table === 'users') {
101
+ // Avoid deleting the admin user
102
+ await transaction(table).del().whereNot('id', '1');
103
+ } else {
104
+ await transaction(table).truncate();
105
+ }
106
+ }
107
+ }
108
+
109
+ async importBasePack(transaction) {
110
+ let baseDataPack = this.baseDataPack;
111
+ if (!path.isAbsolute(this.baseDataPack)) {
112
+ baseDataPack = path.join(getProcessRoot(), baseDataPack);
113
+ }
114
+ let baseData = {};
115
+ try {
116
+ baseData = JSON.parse((await fs.readFile(baseDataPack)).toString());
117
+ this.logger.info('Read base data pack');
118
+ } catch (error) {
119
+ this.logger.error('Failed to read data pack: ', error);
120
+ throw error;
121
+ }
122
+
123
+ this.logger.info('Starting base data import');
124
+ const jsonImporter = new JsonImporter(transaction);
125
+
126
+ // Clear settings table
127
+ await transaction('settings').del();
128
+
129
+ // Hard-coded for order
130
+ const tablesToImport = [
131
+ 'newsletters',
132
+ 'posts',
133
+ 'tags',
134
+ 'products',
135
+ 'benefits',
136
+ 'products_benefits',
137
+ 'stripe_products',
138
+ 'stripe_prices',
139
+ 'settings',
140
+ 'custom_theme_settings'
141
+ ];
142
+ for (const table of tablesToImport) {
143
+ this.logger.info(`Importing content for table ${table} from base data pack`);
144
+ await jsonImporter.import({
145
+ name: table,
146
+ data: baseData[table]
147
+ });
148
+ const tableIndex = this.tableList.findIndex(t => t.name === table);
149
+ if (tableIndex !== -1) {
150
+ this.tableList.splice(tableIndex, 1);
151
+ }
152
+ }
153
+
154
+ this.logger.info('Completed base data import');
155
+ }
156
+
157
+ async importData() {
158
+ const start = Date.now();
159
+
160
+ // Add default tables if none are specified
161
+ if (this.tableList.length === 0) {
162
+ this.tableList = Object.keys(importers).map(name => ({name}));
163
+ } else if (this.withDefault) {
164
+ // Add default tables to the end of the list
165
+ const defaultTables = Object.keys(importers).map(name => ({name}));
166
+ for (const table of defaultTables) {
167
+ if (!this.tableList.find(t => t.name === table.name)) {
168
+ this.tableList.push(table);
169
+ }
170
+ }
171
+ }
172
+
173
+ // Error if we have an unknown table
174
+ for (const table of this.tableList) {
175
+ if (importers[table.name] === undefined) {
176
+ throw new errors.IncorrectUsageError({message: `Unknown table: ${table.name}`});
177
+ }
178
+ }
179
+
180
+ this.sortTableList();
181
+
182
+ if (this.printDependencies) {
183
+ this.logger.info('Table dependencies:');
184
+ for (const table of this.tableList) {
185
+ this.logger.info(`\t${table.name}: ${table.dependencies.join(', ')}`);
186
+ }
187
+ process.exit(0);
188
+ }
189
+
190
+ if (this.useTransaction) {
191
+ await this.knex.transaction(async (transaction) => {
192
+ if (!DatabaseInfo.isSQLite(this.knex)) {
193
+ await transaction.raw('SET autocommit=0;');
194
+ }
195
+
196
+ await this.#run(transaction);
197
+ }, {isolationLevel: 'read committed'});
198
+ } else {
199
+ await this.#run(this.knex);
200
+ }
201
+
202
+ this.logger.info(`Completed data import in ${((Date.now() - start) / 1000).toFixed(1)}s`);
203
+ }
204
+
205
+ async #run(transaction) {
206
+ if (!DatabaseInfo.isSQLite(this.knex)) {
207
+ if (process.env.DISABLE_FAST_IMPORT) {
208
+ await transaction.raw('SET FOREIGN_KEY_CHECKS=0;');
209
+ await transaction.raw('SET unique_checks=0;');
210
+ } else {
211
+ await transaction.raw('ALTER INSTANCE DISABLE INNODB REDO_LOG;');
212
+ await transaction.raw('SET FOREIGN_KEY_CHECKS=0;');
213
+ await transaction.raw('SET unique_checks=0;');
214
+ await transaction.raw('SET GLOBAL local_infile=1;');
215
+ }
216
+ }
217
+
218
+ if (this.willClearData) {
219
+ await this.clearData(transaction);
220
+ }
221
+
222
+ if (this.useBaseDataPack) {
223
+ await this.importBasePack(transaction);
224
+ }
225
+
226
+ // Set quantities for tables
227
+ for (const table of this.tableList) {
228
+ if (this.quantities[table.name] !== undefined) {
229
+ table.quantity = this.quantities[table.name];
230
+ }
231
+ }
232
+
233
+ const cryptoRandomBytes = crypto.randomBytes;
234
+
235
+ if (this.seed) {
236
+ // The probality distributions library uses crypto.randomBytes, which we can't seed, so we need to override it
237
+ crypto.randomBytes = (size) => {
238
+ const buffer = Buffer.alloc(size);
239
+ for (let i = 0; i < size; i++) {
240
+ buffer[i] = Math.floor(faker.datatype.number({min: 0, max: 255}));
241
+ }
242
+ return buffer;
243
+ };
244
+ }
245
+
246
+ try {
247
+ for (const table of this.tableList) {
248
+ if (this.seed) {
249
+ // We reset the seed for every table, so the chosen tables don't affect the data and changes in one importer don't affect the others
250
+ faker.seed(this.seed);
251
+ americanFaker.seed(this.seed);
252
+ }
253
+
254
+ // Add all common options to every importer, whether they use them or not
255
+ const tableImporter = new table.importer(this.knex, transaction, {
256
+ baseUrl: this.baseUrl
257
+ });
258
+
259
+ const amount = table.quantity ?? tableImporter.defaultQuantity;
260
+ this.logger.info('Importing content for table', table.name, amount ? `(${amount} records)` : '');
261
+
262
+ await tableImporter.import(table.quantity ?? undefined);
263
+ }
264
+ } finally {
265
+ if (this.seed) {
266
+ // Revert crypto.randomBytes to the original function
267
+ crypto.randomBytes = cryptoRandomBytes;
268
+ }
269
+ }
270
+
271
+ // Finalise all tables - uses new table importer objects to avoid keeping all data in memory
272
+ for (const table of this.tableList) {
273
+ const tableImporter = new table.importer(this.knex, transaction, {
274
+ baseUrl: this.baseUrl
275
+ });
276
+ await tableImporter.finalise();
277
+ }
278
+
279
+ // Re-enable the redo log because it's a persisted global
280
+ // Leaving it disabled can break the database in the event of an unexpected shutdown
281
+ // See https://dev.mysql.com/doc/refman/8.0/en/innodb-redo-log.html#innodb-disable-redo-logging
282
+ if (!DatabaseInfo.isSQLite(this.knex) && !process.env.DISABLE_FAST_IMPORT) {
283
+ await transaction.raw('ALTER INSTANCE ENABLE INNODB REDO_LOG;');
284
+ }
285
+ }
286
+ }
287
+
288
+ module.exports = DataGenerator;
@@ -0,0 +1,28 @@
1
+ const TableImporter = require('./TableImporter');
2
+ const {faker} = require('@faker-js/faker');
3
+ const {slugify} = require('@tryghost/string');
4
+ const {blogStartDate} = require('../utils/blog-info');
5
+
6
+ class BenefitsImporter extends TableImporter {
7
+ static table = 'benefits';
8
+ static dependencies = [];
9
+ defaultQuantity = 5;
10
+
11
+ constructor(knex, transaction) {
12
+ super(BenefitsImporter.table, knex, transaction);
13
+ }
14
+
15
+ generate() {
16
+ const name = faker.company.catchPhrase();
17
+ const sixMonthsLater = new Date(blogStartDate);
18
+ sixMonthsLater.setMonth(sixMonthsLater.getMonth() + 6);
19
+ return {
20
+ id: this.fastFakeObjectId(),
21
+ name: name,
22
+ slug: `${slugify(name)}-${faker.random.numeric(3)}`,
23
+ created_at: faker.date.between(blogStartDate, sixMonthsLater)
24
+ };
25
+ }
26
+ }
27
+
28
+ module.exports = BenefitsImporter;
@@ -0,0 +1,73 @@
1
+ const {faker} = require('@faker-js/faker');
2
+ const TableImporter = require('./TableImporter');
3
+ const {luck} = require('../utils/random');
4
+ const generateEvents = require('../utils/event-generator');
5
+ const dateToDatabaseString = require('../utils/database-date');
6
+
7
+ class CommentsImporter extends TableImporter {
8
+ static table = 'comments';
9
+ static dependencies = ['posts'];
10
+
11
+ constructor(knex, transaction) {
12
+ super(CommentsImporter.table, knex, transaction);
13
+ }
14
+
15
+ async import(quantity) {
16
+ const posts = await this.transaction.select('id', 'published_at').from('posts')
17
+ .where('status', 'published');
18
+ this.members = await this.transaction.select('id', 'created_at').from('members');
19
+
20
+ this.commentsPerPost = quantity ? quantity / posts.length : 10;
21
+
22
+ await this.importForEach(posts, this.commentsPerPost);
23
+ }
24
+
25
+ setReferencedModel(model) {
26
+ this.model = model;
27
+
28
+ this.commentIds = [];
29
+
30
+ this.timestamps = generateEvents({
31
+ shape: 'ease-out',
32
+ trend: 'negative',
33
+ // Steady readers login more, readers who lose interest read less overall.
34
+ // ceil because members will all have logged in at least once
35
+ total: faker.datatype.number({min: 0, max: this.commentsPerPost}),
36
+ startTime: new Date(model.published_at),
37
+ endTime: new Date()
38
+ });
39
+
40
+ this.possibleMembers = this.members.filter(member => new Date(member.created_at) < new Date(model.published_at));
41
+ }
42
+
43
+ generate() {
44
+ const timestamp = this.timestamps.pop();
45
+ if (!timestamp) {
46
+ // Out of events for this post
47
+ return null;
48
+ }
49
+
50
+ if (this.possibleMembers.length === 0) {
51
+ return null;
52
+ }
53
+
54
+ const isReply = luck(30) && this.commentIds.length > 0; // 30% of comments are replies
55
+
56
+ const event = {
57
+ id: this.fastFakeObjectId(),
58
+ post_id: this.model.id,
59
+ member_id: this.possibleMembers[faker.datatype.number(this.possibleMembers.length - 1)].id,
60
+ parent_id: isReply ? this.commentIds[faker.datatype.number(this.commentIds.length - 1)] : undefined,
61
+ status: 'published',
62
+ created_at: dateToDatabaseString(timestamp),
63
+ updated_at: dateToDatabaseString(timestamp),
64
+ html: `<p>${faker.lorem.sentence().replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)}</p>`
65
+ };
66
+
67
+ this.commentIds.push(event.id);
68
+
69
+ return event;
70
+ }
71
+ }
72
+
73
+ module.exports = CommentsImporter;
@@ -0,0 +1,38 @@
1
+ const TableImporter = require('./TableImporter');
2
+ const {faker} = require('@faker-js/faker');
3
+ const dateToDatabaseString = require('../utils/database-date');
4
+
5
+ class EmailBatchesImporter extends TableImporter {
6
+ static table = 'email_batches';
7
+ static dependencies = ['emails'];
8
+
9
+ constructor(knex, transaction) {
10
+ super(EmailBatchesImporter.table, knex, transaction);
11
+ }
12
+
13
+ async import(quantity) {
14
+ const emails = await this.transaction.select('id', 'created_at', 'email_count').from('emails');
15
+
16
+ // 1 batch per 1000 recipients
17
+ await this.importForEach(emails, quantity ?? (() => {
18
+ return Math.ceil(this.model.email_count / 1000);
19
+ }));
20
+ }
21
+
22
+ generate() {
23
+ const emailSentDate = new Date(this.model.created_at);
24
+ const latestUpdatedDate = new Date(this.model.created_at);
25
+ latestUpdatedDate.setHours(latestUpdatedDate.getHours() + 1);
26
+
27
+ return {
28
+ id: this.fastFakeObjectId(),
29
+ email_id: this.model.id,
30
+ provider_id: `${new Date().toISOString().split('.')[0].replace(/[^0-9]/g, '')}.${faker.datatype.hexadecimal({length: 16, prefix: '', case: 'lower'})}@m.example.com`,
31
+ status: 'submitted', // TODO: introduce failures
32
+ created_at: this.model.created_at,
33
+ updated_at: dateToDatabaseString(faker.date.between(emailSentDate, latestUpdatedDate))
34
+ };
35
+ }
36
+ }
37
+
38
+ module.exports = EmailBatchesImporter;
@@ -0,0 +1,67 @@
1
+ const TableImporter = require('./TableImporter');
2
+ const {faker} = require('@faker-js/faker');
3
+
4
+ class EmailRecipientFailuresImporter extends TableImporter {
5
+ static table = 'email_recipient_failures';
6
+ static dependencies = ['email_recipients'];
7
+
8
+ constructor(knex, transaction) {
9
+ super(EmailRecipientFailuresImporter.table, knex, transaction);
10
+ }
11
+
12
+ async import(quantity) {
13
+ const recipients = await this.transaction
14
+ .select(
15
+ 'id',
16
+ 'email_id',
17
+ 'member_id',
18
+ 'failed_at')
19
+ .from('email_recipients')
20
+ .whereNotNull('failed_at');
21
+
22
+ await this.importForEach(recipients, quantity ? quantity / recipients.length : 1);
23
+ }
24
+
25
+ generate() {
26
+ const errors = [
27
+ {
28
+ severity: 'permanent',
29
+ code: 605,
30
+ enhanced_code: null,
31
+ message: 'Not delivering to previously bounced address'
32
+ },
33
+ {
34
+ severity: 'permanent',
35
+ code: 451,
36
+ enhanced_code: '4.7.652',
37
+ message: '4.7.652 The mail server [xxx.xxx.xxx.xxx] has exceeded the maximum number of connections.'
38
+ },
39
+ {
40
+ message: 'No MX for example.com',
41
+ code: 498,
42
+ enhanced_code: null,
43
+ severity: 'permanent'
44
+ },
45
+ {
46
+ severity: 'temporary',
47
+ code: 552,
48
+ enhanced_code: null,
49
+ message: '5.2.2 <xxxxxxxx@example.com>: user is over quota'
50
+ }
51
+ ];
52
+
53
+ const error = faker.helpers.arrayElement(errors);
54
+
55
+ return {
56
+ id: this.fastFakeObjectId(),
57
+ email_id: this.model.email_id,
58
+ member_id: this.model.member_id,
59
+ email_recipient_id: this.model.id,
60
+ event_id: faker.random.alphaNumeric(20),
61
+ ...error,
62
+ failed_at: this.model.failed_at
63
+ };
64
+ }
65
+ }
66
+
67
+ module.exports = EmailRecipientFailuresImporter;