ghost 6.54.0 → 6.55.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 (327) hide show
  1. package/components/ghost-storage-base-3.0.0.tgz +0 -0
  2. package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
  3. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  4. package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
  5. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  6. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  7. package/core/boot.js +3 -6
  8. package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-sjHLx5dy.js} +1 -1
  9. package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-DoYXtnZ8.js} +4 -4
  10. package/core/built/admin/assets/activitypub/activitypub.js +2 -2
  11. package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
  12. package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
  13. package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-D8sFfdpC.js} +2 -2
  14. package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-CWueFW6q.js} +5 -5
  15. package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BGxPU7N5.js} +3 -3
  16. package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-DBK-u0Ch.js} +3 -3
  17. package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-Bcir5RQZ.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-BV-CIT3d.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-yC0nfvzl.js} +4 -4
  20. package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-DTRS9TDk.js} +2 -2
  21. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
  22. package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-Dwl9t0pb.js} +4 -4
  23. package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-B-zKsZ7r.js} +3 -3
  24. package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-DjJZD54t.js} +3 -3
  25. package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-CoU8vs6d.js} +6 -6
  26. package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-BAsaJekx.js} +6 -6
  27. package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CLH3OVTP.js} +28 -25
  28. package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-DA6pNJ-Q.js} +3 -3
  29. package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-CQD42Pw5.js} +2 -2
  30. package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-Cg0saow1.js} +7 -7
  31. package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
  32. package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
  33. package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-DRy7qY-J.js} +2 -2
  34. package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-Bx3VcFIL.js} +1 -1
  35. package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
  36. package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
  37. package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-BD0U2ygg.js} +1 -1
  38. package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-fjQKRVyY.js} +1 -1
  39. package/core/built/admin/assets/checkbox-CWa28lgO.js +1 -0
  40. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
  41. package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
  42. package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
  43. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +25 -0
  44. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +27 -0
  45. package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +1 -0
  46. package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
  47. package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-BFp58EMk.js} +1 -1
  48. package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
  49. package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
  50. package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
  51. package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
  52. package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
  53. package/core/built/admin/assets/dropzone-DlK-K2DQ.js +5 -0
  54. package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-BKT-REJw.js} +1 -1
  55. package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
  56. package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-ClUpJZOp.js} +1 -1
  57. package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-DF92dUMP.js} +1 -1
  58. package/core/built/admin/assets/field-Do7lieBA.js +1 -0
  59. package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-BV2rnpfA.js} +1 -1
  60. package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
  61. package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-DfbIWc_l.js} +1 -1
  62. package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
  63. package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-c4057474943effc568c5659ccc4909d7.js} +407 -406
  64. package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
  65. package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
  66. package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
  67. package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
  68. package/core/built/admin/assets/{Growth-CSaHoSVD.js → growth-CVw1My4S.js} +2 -2
  69. package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
  70. package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-CFBSu4Ed.js} +12 -12
  71. package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
  72. package/core/built/admin/assets/index-BGG7NckO.js +67 -0
  73. package/core/built/admin/assets/input-group-BSgG_lVw.js +1 -0
  74. package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
  75. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
  76. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
  77. package/core/built/admin/assets/koenig-lexical/style.css +1 -1
  78. package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-f8vHRYvz.js} +28 -28
  79. package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DSap9rAy.js} +1 -1
  80. package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-DU0rn9Cj.js} +1 -1
  81. package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-Dz33AV6h.js} +1 -1
  82. package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-BtTTrKvJ.js} +1 -1
  83. package/core/built/admin/assets/{label-BOR9XdKl.js → label-DS8Mi6G5.js} +1 -1
  84. package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
  85. package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
  86. package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
  87. package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
  88. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
  89. package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
  90. package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
  91. package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
  92. package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
  93. package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
  94. package/core/built/admin/assets/{Newsletters-C5cs6FGo.js → newsletters-B3QQXfGT.js} +1 -1
  95. package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
  96. package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
  97. package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
  98. package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
  99. package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
  100. package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
  101. package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
  102. package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-B3Wb9Jqv.js} +1 -1
  103. package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
  104. package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-We-ko5Cb.js} +1 -1
  105. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
  106. package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
  107. package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-DFfO93_0.js} +1 -1
  108. package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
  109. package/core/built/admin/assets/{preferences-D4EANGoc.js → preferences-DJPfHEGu.js} +2 -2
  110. package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
  111. package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
  112. package/core/built/admin/assets/select-C5_xEt5b.js +1 -0
  113. package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
  114. package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-WXNeWyU1.js} +1 -1
  115. package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
  116. package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-DsgW3wbg.js} +1 -1
  117. package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
  118. package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-Bb_FCl7e.js} +1 -1
  119. package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-BeaRxUn9.js} +1 -1
  120. package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-Bjj_kmJ1.js} +1 -1
  121. package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-BxPbW6K1.js} +1 -1
  122. package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-B1e3-1F8.js} +1 -1
  123. package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-C5WaeBJo.js} +1 -1
  124. package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-B46iVPwu.js} +2 -2
  125. package/core/built/admin/assets/switch-z5Y6Uzt6.js +1 -0
  126. package/core/built/admin/assets/{table-B8LgTQJZ.js → table-CB-4_HBJ.js} +1 -1
  127. package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
  128. package/core/built/admin/assets/{tags-B4l891z2.js → tags-Cg2VJGmG.js} +1 -1
  129. package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
  130. package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
  131. package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-b3IlxvhV.js} +1 -1
  132. package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-DAFT87tQ.js} +1 -1
  133. package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-S0FTd43s.js} +1 -1
  134. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
  135. package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-CIaiHOVb.js} +1 -1
  136. package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-Cq2vBPrA.js} +1 -1
  137. package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
  138. package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-CjYNxdwe.js} +1 -1
  139. package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-falHAtuK.js} +1 -1
  140. package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-D7-8ND1y.js} +1 -1
  141. package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
  142. package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-CvnC9zF2.js} +1 -1
  143. package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
  144. package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
  145. package/core/built/admin/index.html +14 -10
  146. package/core/frontend/services/proxy.js +10 -1
  147. package/core/frontend/services/routing/router-manager.js +14 -14
  148. package/core/frontend/services/routing/static-routes-router.js +8 -10
  149. package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
  150. package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
  151. package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
  152. package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
  153. package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
  154. package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
  155. package/core/server/adapters/storage/LocalStorageBase.js +107 -167
  156. package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
  157. package/core/server/adapters/storage/utils.js +36 -50
  158. package/core/server/adapters/storage/utils.ts +60 -0
  159. package/core/server/api/endpoints/automation-action-links.js +68 -0
  160. package/core/server/api/endpoints/automation-action-links.ts +46 -0
  161. package/core/server/api/endpoints/db.js +6 -0
  162. package/core/server/api/endpoints/index.js +4 -0
  163. package/core/server/api/endpoints/members.js +25 -28
  164. package/core/server/api/endpoints/pages.js +6 -1
  165. package/core/server/api/endpoints/posts.js +11 -2
  166. package/core/server/api/endpoints/settings.js +0 -2
  167. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
  168. package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
  169. package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
  170. package/core/server/data/exporter/export-filename.js +2 -1
  171. package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
  172. package/core/server/data/importer/importers/json-to-html.js +24 -4
  173. package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
  174. package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
  175. package/core/server/data/schema/commands.js +5 -0
  176. package/core/server/data/schema/default-settings/default-settings.json +0 -4
  177. package/core/server/data/schema/schema.js +5 -1
  178. package/core/server/lib/image/image-size.js +3 -17
  179. package/core/server/lib/image/image-utils.js +15 -1
  180. package/core/server/lib/request-external.js +1 -1
  181. package/core/server/models/user.js +46 -70
  182. package/core/server/services/auth/session/session-service.js +28 -1
  183. package/core/server/services/automations/automations-api.js +21 -6
  184. package/core/server/services/automations/automations-api.ts +25 -6
  185. package/core/server/services/automations/automations-repository.ts +19 -0
  186. package/core/server/services/automations/database-automations-repository.js +116 -28
  187. package/core/server/services/automations/database-automations-repository.ts +118 -9
  188. package/core/server/services/automations/database-date.js +10 -0
  189. package/core/server/services/automations/database-date.ts +5 -0
  190. package/core/server/services/automations/poll.js +5 -2
  191. package/core/server/services/automations/poll.ts +7 -2
  192. package/core/server/services/automations/scheduler-poll-time.js +40 -0
  193. package/core/server/services/automations/scheduler-poll-time.ts +38 -0
  194. package/core/server/services/automations/service.js +15 -3
  195. package/core/server/services/automations/service.ts +16 -3
  196. package/core/server/services/comments/comments-service-emails.js +14 -8
  197. package/core/server/services/comments/comments-service.js +9 -4
  198. package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
  199. package/core/server/services/email-service/email-service-wrapper.js +6 -1
  200. package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
  201. package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
  202. package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
  203. package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
  204. package/core/server/services/link-redirection/link-redirect.js +2 -0
  205. package/core/server/services/link-redirection/link-redirect.ts +3 -1
  206. package/core/server/services/link-redirection/link-redirects-service.js +38 -2
  207. package/core/server/services/link-tracking/click-event.js +3 -0
  208. package/core/server/services/link-tracking/index.js +38 -6
  209. package/core/server/services/link-tracking/link-click-repository.js +30 -9
  210. package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
  211. package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
  212. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
  213. package/core/server/services/member-welcome-emails/service.js +11 -3
  214. package/core/server/services/members/{importer → import-export}/config.js +4 -9
  215. package/core/server/services/members/{importer → import-export}/config.ts +4 -9
  216. package/core/server/services/members/import-export/csv/formula.js +16 -0
  217. package/core/server/services/members/import-export/csv/formula.ts +14 -0
  218. package/core/server/services/members/import-export/csv/index.js +12 -0
  219. package/core/server/services/members/import-export/csv/index.ts +3 -0
  220. package/core/server/services/members/import-export/csv/parse.js +64 -0
  221. package/core/server/services/members/import-export/csv/parse.ts +69 -0
  222. package/core/server/services/members/import-export/csv/serialize.js +13 -0
  223. package/core/server/services/members/import-export/csv/serialize.ts +18 -0
  224. package/core/server/services/members/import-export/export/exporter.js +202 -0
  225. package/core/server/services/members/import-export/export/exporter.ts +325 -0
  226. package/core/server/services/members/import-export/import/completion-email.js +84 -0
  227. package/core/server/services/members/import-export/import/completion-email.ts +141 -0
  228. package/core/server/services/members/import-export/import/importer.js +297 -0
  229. package/core/server/services/members/import-export/import/importer.ts +468 -0
  230. package/core/server/services/members/import-export/import/reader.js +32 -0
  231. package/core/server/services/members/import-export/import/reader.ts +33 -0
  232. package/core/server/services/members/import-export/import/row.js +33 -0
  233. package/core/server/services/members/import-export/import/row.ts +52 -0
  234. package/core/server/services/members/import-export/import/spool.js +30 -0
  235. package/core/server/services/members/import-export/import/spool.ts +38 -0
  236. package/core/server/services/members/import-export/index.js +99 -0
  237. package/core/server/services/members/import-export/index.ts +137 -0
  238. package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
  239. package/core/server/services/members/service.js +25 -20
  240. package/core/server/services/members-custom-fields/values-service.js +19 -7
  241. package/core/server/services/members-custom-fields/values-service.ts +19 -7
  242. package/core/server/services/mentions/service.js +5 -1
  243. package/core/server/services/posts/posts-exporter.js +6 -4
  244. package/core/server/services/posts/posts-service.js +2 -2
  245. package/core/server/services/route-settings/activation-bridge.js +59 -49
  246. package/core/server/services/route-settings/activation-bridge.ts +91 -51
  247. package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
  248. package/core/server/services/route-settings/index.js +1 -16
  249. package/core/server/services/settings/settings-service.js +0 -18
  250. package/core/server/services/themes/storage.js +9 -0
  251. package/core/server/services/themes/theme-storage.js +1 -1
  252. package/core/server/services/url/url-service-facade.js +1 -1
  253. package/core/server/services/url/url-service-facade.ts +8 -1
  254. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  255. package/core/shared/config/defaults.json +2 -1
  256. package/core/shared/express.js +6 -1
  257. package/core/shared/labs.js +2 -1
  258. package/package.json +11 -11
  259. package/pnpm-lock.yaml +18 -18
  260. package/pnpm-workspace.yaml +8 -7
  261. package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
  262. package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
  263. package/core/built/admin/assets/Web-_fD935OX.js +0 -2
  264. package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
  265. package/core/built/admin/assets/audience-BKRdsFmy.js +0 -1
  266. package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
  267. package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
  268. package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
  269. package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
  270. package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
  271. package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
  272. package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
  273. package/core/built/admin/assets/domain-B7_n5nfx.js +0 -1
  274. package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
  275. package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
  276. package/core/built/admin/assets/field-BW-M2Zwp.js +0 -1
  277. package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
  278. package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +0 -1
  279. package/core/built/admin/assets/growth-Ck2s3JjR.js +0 -1
  280. package/core/built/admin/assets/index-BBf502Wg.js +0 -69
  281. package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
  282. package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
  283. package/core/built/admin/assets/label-picker-BdNAWuUX.js +0 -1
  284. package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
  285. package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
  286. package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +0 -1
  287. package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
  288. package/core/built/admin/assets/members-DsPxYAVm.js +0 -10
  289. package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
  290. package/core/built/admin/assets/moderation-Dlp7M5sa.js +0 -1
  291. package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +0 -1
  292. package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
  293. package/core/built/admin/assets/note-BrAZ7Z0o.js +0 -1
  294. package/core/built/admin/assets/notifications-BnbHLOPC.js +0 -1
  295. package/core/built/admin/assets/onboarding-route-eU146kuA.js +0 -1
  296. package/core/built/admin/assets/overview-BpbaK-xu.js +0 -1
  297. package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
  298. package/core/built/admin/assets/post-analytics-provider-BDqcfBor.js +0 -1
  299. package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
  300. package/core/built/admin/assets/select--2uVN9_-.js +0 -1
  301. package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
  302. package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
  303. package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
  304. package/core/built/admin/assets/tabs-D3ZOYkM7.js +0 -1
  305. package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
  306. package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
  307. package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
  308. package/core/server/services/mail/templates/newsletter.html +0 -876
  309. package/core/server/services/members/csv/index.js +0 -10
  310. package/core/server/services/members/csv/index.ts +0 -3
  311. package/core/server/services/members/csv/parse.js +0 -94
  312. package/core/server/services/members/csv/parse.ts +0 -111
  313. package/core/server/services/members/csv/types.js +0 -2
  314. package/core/server/services/members/csv/types.ts +0 -39
  315. package/core/server/services/members/csv/unparse.js +0 -69
  316. package/core/server/services/members/csv/unparse.ts +0 -79
  317. package/core/server/services/members/exporter/query.js +0 -233
  318. package/core/server/services/members/importer/index.js +0 -30
  319. package/core/server/services/members/importer/labels.js +0 -125
  320. package/core/server/services/members/importer/members-csv-importer.js +0 -520
  321. package/core/server/services/route-settings/default-settings-manager.js +0 -63
  322. package/core/server/services/route-settings/route-settings.js +0 -170
  323. package/core/server/services/route-settings/settings-loader.js +0 -60
  324. package/core/server/services/route-settings/settings-path-manager.js +0 -47
  325. package/core/server/services/route-settings/validate.js +0 -440
  326. /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
  327. /package/core/server/services/members/{importer/members-csv-importer-stripe-utils.js → import-export/import/stripe-utils.js} +0 -0
@@ -3,14 +3,27 @@
3
3
  const moment = require('moment-timezone');
4
4
  const errors = require('@tryghost/errors');
5
5
  const logging = require('@tryghost/logging');
6
- const models = require('../../models');
7
6
  const membersService = require('../../services/members');
8
7
 
9
- const settingsCache = require('../../../shared/settings-cache');
10
8
  const tpl = require('@tryghost/tpl');
11
9
  const _ = require('lodash');
12
10
  const {getCSVExportFileName} = require('./utils/csv-export-filename');
13
11
 
12
+ // Shape the import service's outcome into the API response envelope: an inline import
13
+ // reports its stats and label, a deferred one only how much it accepted.
14
+ function importCSVResponse(outcome) {
15
+ if (outcome.deferred) {
16
+ return {meta: {originalImportSize: outcome.originalImportSize}};
17
+ }
18
+ return {
19
+ meta: {
20
+ originalImportSize: outcome.originalImportSize,
21
+ stats: {imported: outcome.result.imported, invalid: outcome.result.errors},
22
+ import_label: outcome.result.importLabel || null
23
+ }
24
+ };
25
+ }
26
+
14
27
  const messages = {
15
28
  memberNotFound: 'Member not found.',
16
29
  notSendingWelcomeEmail: 'Email verification required, welcome email is disabled',
@@ -418,33 +431,17 @@ const controller = {
418
431
  method: 'add'
419
432
  },
420
433
  async query(frame) {
421
- const siteTimezone = settingsCache.get('timezone');
422
-
423
- const importLabel = {
424
- name: `Import ${moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm')}`
425
- };
426
-
427
- const globalLabels = [importLabel].concat(frame.data.labels);
428
- const pathToCSV = frame.file.path;
429
- const headerMapping = frame.data.mapping;
430
-
431
- let email;
432
- if (frame.user) {
433
- email = frame.user.get('email');
434
- } else {
435
- email = (await models.User.getOwnerUser()).get('email');
436
- }
437
-
438
- return membersService.processImport({
439
- pathToCSV,
440
- headerMapping,
441
- globalLabels,
442
- importLabel,
443
- LabelModel: models.Label,
444
- user: {
445
- email: email
446
- }
434
+ // The endpoint adapts the request frame into the import service's arguments
435
+ // and shapes its outcome into the response envelope, so neither the frame nor
436
+ // the meta shape crosses into the domain. The recipient is the request user;
437
+ // the service falls back to the site owner when there is none.
438
+ const outcome = await membersService.importCSV({
439
+ filePath: frame.file.path,
440
+ mapping: frame.data.mapping,
441
+ extraLabels: frame.data.labels || [],
442
+ requestUserEmail: frame.user ? frame.user.get('email') : null
447
443
  });
444
+ return importCSVResponse(outcome);
448
445
  }
449
446
  },
450
447
 
@@ -2,6 +2,7 @@ const models = require('../../models');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const getPostServiceInstance = require('../../services/posts/posts-service-instance');
5
+ const {rejectAdminApiRestrictedFieldsTransformer} = require('./utils/api-filter-utils');
5
6
  const ALLOWED_INCLUDES = ['tags', 'authors', 'authors.roles', 'tiers', 'count.signups', 'count.paid_conversions', 'post_revisions', 'post_revisions.author'];
6
7
  const UNSAFE_ATTRS = ['status', 'authors', 'visibility'];
7
8
 
@@ -44,7 +45,11 @@ const controller = {
44
45
  unsafeAttrs: UNSAFE_ATTRS
45
46
  },
46
47
  query(frame) {
47
- return models.Post.findPage(frame.options);
48
+ const options = {
49
+ ...frame.options,
50
+ mongoTransformer: rejectAdminApiRestrictedFieldsTransformer
51
+ };
52
+ return models.Post.findPage(options);
48
53
  }
49
54
  },
50
55
 
@@ -2,6 +2,7 @@ const urlUtils = require('../../../shared/url-utils').default;
2
2
  const models = require('../../models');
3
3
  const {getCSVExportFileName} = require('./utils/csv-export-filename');
4
4
  const getPostServiceInstance = require('../../services/posts/posts-service-instance');
5
+ const {rejectAdminApiRestrictedFieldsTransformer} = require('./utils/api-filter-utils');
5
6
  const allowedIncludes = [
6
7
  'tags',
7
8
  'authors',
@@ -78,7 +79,11 @@ const controller = {
78
79
  unsafeAttrs: unsafeAttrs
79
80
  },
80
81
  query(frame) {
81
- return postsService.browsePosts(frame.options);
82
+ const options = {
83
+ ...frame.options,
84
+ mongoTransformer: rejectAdminApiRestrictedFieldsTransformer
85
+ };
86
+ return postsService.browsePosts(options);
82
87
  }
83
88
  },
84
89
 
@@ -106,8 +111,12 @@ const controller = {
106
111
  },
107
112
  validation: {},
108
113
  async query(frame) {
114
+ const options = {
115
+ ...frame.options,
116
+ mongoTransformer: rejectAdminApiRestrictedFieldsTransformer
117
+ };
109
118
  return {
110
- data: await postsService.export(frame),
119
+ data: await postsService.export(options),
111
120
  filename: getCSVExportFileName('analytics')
112
121
  };
113
122
  }
@@ -174,8 +174,6 @@ const controller = {
174
174
  async query(frame) {
175
175
  const content = await fs.readFile(frame.file.path, 'utf8');
176
176
  await routeSettings.api.upload(content);
177
- const getRoutesHash = () => routeSettings.api.getCurrentHash();
178
- await settingsService.syncRoutesHash(getRoutesHash);
179
177
  }
180
178
  },
181
179
 
@@ -120,7 +120,10 @@ const commentMapper = (model, frame) => {
120
120
 
121
121
  if (jsonModel.post) {
122
122
  // We could use the post mapper here, but we need less field + don't need all the async behavior support
123
- url.forPost(jsonModel.post.id, jsonModel.post, frame);
123
+ // Comments live on pages as well as posts; the URL service routes by
124
+ // the passed type, so a page typed 'posts' resolves to /404/.
125
+ const routerType = jsonModel.post.type === 'page' ? 'pages' : 'posts';
126
+ url.forPost(jsonModel.post.id, jsonModel.post, frame, routerType);
124
127
  response.post = _.pick(jsonModel.post, postFields);
125
128
 
126
129
  // Compute excerpt from custom_excerpt or plaintext (same logic as post serializer)
@@ -1,10 +1,12 @@
1
1
  //@ts-check
2
2
  const _ = require('lodash');
3
3
  const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:members');
4
- const {unparse} = require('../../../../../services/members/csv');
4
+ const {serialize: serializeCSV} = require('../../../../../services/members/import-export/csv');
5
+ // The export CSV row shape and its encode live in the domain (typed against the export
6
+ // row) rather than here, so a change to the export row is caught by the compiler.
7
+ const {toExportCsvRow} = require('../../../../../services/members/import-export/export/exporter');
5
8
  const mappers = require('./mappers');
6
9
  const {Transform} = require('stream');
7
- const papaparse = require('papaparse');
8
10
  const {createCSVStreamResponse} = require('./stream-csv-response');
9
11
  module.exports = {
10
12
  browse: createSerializer('browse', paginatedMembers),
@@ -25,56 +27,6 @@ module.exports = {
25
27
  activityFeed: createSerializer('activityFeed', activityFeed)
26
28
  };
27
29
 
28
- /**
29
- * Formats a single member for CSV export
30
- * @param {Object} member - Member object
31
- * @returns {Object} Formatted member
32
- */
33
- function formatMemberForCSV(member) {
34
- let labels = '';
35
- if (Array.isArray(member.labels)) {
36
- labels = member.labels.map((l) => {
37
- return typeof l === 'string' ? l : l.name;
38
- }).join(',');
39
- }
40
-
41
- let tiers = '';
42
- if (Array.isArray(member.tiers)) {
43
- tiers = member.tiers.map((tier) => {
44
- return tier.name;
45
- }).join(',');
46
- }
47
-
48
- // Convert boolean 'false' to empty string for tests to pass
49
- // Only comped = true should result in 'true', otherwise empty string
50
- const complimentaryPlan = member.comped === true ? 'true' : '';
51
-
52
- // Gift members carry the gift id so an exported CSV can be re-imported and reassigned
53
- // back to a (possibly new) member record via the gifts table
54
- const giftId = member.gift_id || '';
55
-
56
- // Convert subscribed boolean to string representation
57
- const subscribedToEmails = member.subscribed === true ? 'true' : 'false';
58
-
59
- return {
60
- id: member.id,
61
- email: member.email,
62
- name: member.name,
63
- note: member.note,
64
- subscribed_to_emails: subscribedToEmails,
65
- complimentary_plan: complimentaryPlan,
66
- stripe_customer_id: member.stripe_customer_id,
67
- created_at: member.created_at,
68
- deleted_at: member.deleted_at || null,
69
- labels: labels,
70
- tiers: tiers,
71
- gift_id: giftId,
72
- // The exporter flattens custom field values into their CSV columns, since
73
- // which columns exist is a per-site question only the database can answer.
74
- ...member.custom_field_cells
75
- };
76
- }
77
-
78
30
  /**
79
31
  * @template PageMeta
80
32
  *
@@ -426,30 +378,16 @@ function createCSVTransform() {
426
378
  transform(member, encoding, callback) {
427
379
  try {
428
380
  // Format the member data for CSV
429
- const formattedMember = formatMemberForCSV(member);
381
+ const formattedMember = toExportCsvRow(member);
430
382
 
431
383
  // For first chunk, include the headers
432
384
  if (fields === null) {
433
385
  fields = Object.keys(formattedMember);
434
- const csv = papaparse.unparse({
435
- fields,
436
- data: [formattedMember]
437
- }, {
438
- header: true,
439
- escapeFormulae: true,
440
- newline: '\r\n' // Explicitly set Windows-style line endings for compatibility
441
- });
386
+ const csv = serializeCSV([formattedMember], {columns: fields, header: true});
442
387
  callback(null, csv);
443
388
  } else {
444
389
  // For subsequent chunks, don't include headers, just the data
445
- const csv = papaparse.unparse({
446
- fields,
447
- data: [formattedMember]
448
- }, {
449
- header: false,
450
- escapeFormulae: true,
451
- newline: '\r\n' // Explicitly set Windows-style line endings for compatibility
452
- });
390
+ const csv = serializeCSV([formattedMember], {columns: fields, header: false});
453
391
 
454
392
  // Make sure each row starts with a newline to ensure separation between rows
455
393
  // Ensure consistent line endings by using explicit CR+LF sequence
@@ -465,28 +403,21 @@ function createCSVTransform() {
465
403
  /**
466
404
  * @template PageMeta
467
405
  *
468
- * @param {{data: any[]|Object, filename?: string}} data
406
+ * @param {{data: NodeJS.ReadableStream, filename?: string}} data
469
407
  *
470
- * @returns {string|Function} - A CSV string or response handler function
408
+ * @returns {Function} - A response handler that streams the CSV
471
409
  */
472
410
  function exportCSV(data) {
473
411
  debug('exportCSV');
474
412
 
475
- // Check if data.data is a stream (has the pipe method)
476
- if (data.data && typeof data.data.pipe === 'function') {
477
- debug('CSV stream response');
413
+ // The export endpoint always yields a stream (response.stream: true), so there is
414
+ // only ever a stream to pipe.
415
+ const datetime = (new Date()).toJSON().substring(0, 10);
416
+ const filename = data.filename || `members.${datetime}.csv`;
478
417
 
479
- // Fall back to the legacy filename if the endpoint didn't provide one
480
- const datetime = (new Date()).toJSON().substring(0, 10);
481
- const filename = data.filename || `members.${datetime}.csv`;
482
-
483
- return createCSVStreamResponse({
484
- source: data.data,
485
- transform: createCSVTransform(),
486
- filename
487
- });
488
- }
489
-
490
- // Otherwise use the unparse function for array data
491
- return unparse(data.data);
418
+ return createCSVStreamResponse({
419
+ source: data.data,
420
+ transform: createCSVTransform(),
421
+ filename
422
+ });
492
423
  }
@@ -24,7 +24,17 @@ const forPost = (id, attrs, frame, type = 'posts') => {
24
24
  return attrs;
25
25
  }
26
26
 
27
- attrs.url = urlService.facade.getUrlForResource({...attrs, id, type}, {absolute: true});
27
+ // Diagnostic only: hand the compare-mode URL service the producing api
28
+ // endpoint. The parity/thin-resource logs capture a caller stack, but it
29
+ // truncates at the async api-framework boundary and never names the
30
+ // endpoint; the api-framework Frame carries it, so pass it through. Omitted
31
+ // for non-api callers (mentions, email) whose frame has no endpoint fields.
32
+ const options = {absolute: true};
33
+ if (frame && (frame.docName || frame.method)) {
34
+ options.serializerContext = {apiType: frame.apiType, docName: frame.docName, method: frame.method};
35
+ }
36
+
37
+ attrs.url = urlService.facade.getUrlForResource({...attrs, id, type}, options);
28
38
 
29
39
  /**
30
40
  * CASE: admin api should serve preview urls
@@ -3,6 +3,7 @@ const logging = require('@tryghost/logging');
3
3
  const errors = require('@tryghost/errors');
4
4
  const security = require('@tryghost/security');
5
5
  const models = require('../../models');
6
+ const path = require('path');
6
7
 
7
8
  const modelOptions = {context: {internal: true}};
8
9
 
@@ -14,7 +15,7 @@ const exportFileName = async function exportFileName(options) {
14
15
 
15
16
  // custom filename
16
17
  if (options.filename) {
17
- return options.filename + '.json';
18
+ return path.basename(options.filename) + '.json';
18
19
  }
19
20
 
20
21
  try {
@@ -6,7 +6,6 @@ const path = require('path');
6
6
  const fs = require('fs-extra');
7
7
 
8
8
  const config = require('../../../../../shared/config');
9
- const models = require('../../../../models');
10
9
 
11
10
  class RevueSubscriberImporter extends BaseImporter {
12
11
  constructor(allDataFromFile) {
@@ -38,17 +37,14 @@ class RevueSubscriberImporter extends BaseImporter {
38
37
  const outputFilePath = path.join(config.getContentPath('data'), '/', outputFileName);
39
38
  const csvData = papaparse.unparse(this.dataToImport);
40
39
 
41
- const memberImporterOptions = {
42
- pathToCSV: outputFilePath,
43
- globalLabels: [{name: importLabel}],
44
- importLabel: {name: importLabel},
45
- LabelModel: models.Label,
46
- forceInline: true
47
- };
48
-
49
40
  await fs.writeFile(outputFilePath, csvData);
50
41
 
51
- return membersService.processImport(memberImporterOptions);
42
+ // Inline: the data import has no request to hold open and needs the result now.
43
+ return membersService.importInline({
44
+ filePath: outputFilePath,
45
+ extraLabels: [{name: importLabel}],
46
+ requestUserEmail: null
47
+ });
52
48
  }
53
49
  }
54
50
 
@@ -4,6 +4,25 @@ const {cards} = require('@tryghost/kg-default-cards');
4
4
  const imageCard = cards.find(c => c.name === 'image');
5
5
  const embedCard = cards.find(c => c.name === 'embed');
6
6
 
7
+ const getValidURL = (url) => {
8
+ const normalizedURL = typeof url === 'string' ? url.trim() : '';
9
+
10
+ if (!normalizedURL) {
11
+ return '';
12
+ }
13
+
14
+ try {
15
+ const parsedURL = new URL(normalizedURL, 'https://example.com');
16
+ if (parsedURL.protocol === 'http:' || parsedURL.protocol === 'https:') {
17
+ return normalizedURL;
18
+ }
19
+ } catch {
20
+ // Invalid URLs are omitted from imported links
21
+ }
22
+
23
+ return '';
24
+ };
25
+
7
26
  // Take the array of items for a specific post and return the converted HTML
8
27
  const itemsToHtml = (items) => {
9
28
  let itemHTMLChunks = [];
@@ -30,22 +49,23 @@ const itemsToHtml = (items) => {
30
49
  } else if (type === 'link') {
31
50
  // This could be a bookmark, or it could be a paragraph of text with a linked header, there's no way to tell
32
51
  // The safest option here is to output an image with text under it
52
+ const itemURL = getValidURL(item.url);
33
53
  let cardOpts = {
34
54
  env: {dom: new SimpleDom.Document()},
35
55
  payload: {
36
56
  src: item.image,
37
57
  caption: item.title,
38
- href: item.url
58
+ href: itemURL
39
59
  }
40
60
  };
41
61
  itemHTMLChunks.push(serializer.serialize(imageCard.render(cardOpts)));
42
-
43
- let linkHTML = `<h4><a href="${item.url}">${item.title}</a></h4>${item.description}`;
62
+ const linkTitleHTML = itemURL ? `<a href="${serializer.escapeAttrValue(itemURL)}">${item.title}</a>` : item.title;
63
+ let linkHTML = `<h4>${linkTitleHTML}</h4>${item.description}`;
44
64
  itemHTMLChunks.push(linkHTML);
45
65
  } else if (type === 'tweet') {
46
66
  // Should this be an oEmbed call? Probably.
47
67
  itemHTMLChunks.push(`<figure class="kg-card kg-embed-card">
48
- <blockquote class="twitter-tweet"><a href="${item.url}"></a></blockquote>
68
+ <blockquote class="twitter-tweet"><a href="${serializer.escapeAttrValue(item.url)}"></a></blockquote>
49
69
  <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
50
70
  </figure>`);
51
71
  } else if (type === 'video') {
@@ -0,0 +1,51 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {combineNonTransactionalMigrations, createAddColumnMigration, createNonTransactionalMigration} = require('../../utils');
3
+ const {addIndex, addUnique, dropIndex, dropUnique, getIndexes} = require('../../../schema/commands');
4
+
5
+ const uniqueColumns = ['automation_action_revision_id', 'to_hash'];
6
+ const leftoverIndexName = 'redirects_automation_action_revision_id_index';
7
+
8
+ module.exports = combineNonTransactionalMigrations(
9
+ createAddColumnMigration('redirects', 'to_hash', {
10
+ type: 'binary',
11
+ maxlength: 32,
12
+ nullable: true
13
+ }, {algorithm: 'auto'}),
14
+ createNonTransactionalMigration(
15
+ async function up(knex) {
16
+ await addUnique('redirects', uniqueColumns, knex);
17
+
18
+ // A previous down() leaves a standalone index behind (see below).
19
+ // InnoDB drops the index it created for the foreign key itself once
20
+ // the composite unique can serve the constraint, but not one we
21
+ // created, so drop it here to keep a rolled-back-and-remigrated
22
+ // database in the same shape as every other one.
23
+ const indexes = await getIndexes('redirects', knex);
24
+
25
+ if (indexes.includes(leftoverIndexName)) {
26
+ await dropIndex('redirects', ['automation_action_revision_id'], knex);
27
+ }
28
+ },
29
+ async function down(knex) {
30
+ try {
31
+ await dropUnique('redirects', uniqueColumns, knex);
32
+ } catch (err) {
33
+ // The composite unique ends up as the only index covering the
34
+ // automation_action_revision_id FK: fresh installs create it
35
+ // that way, and on upgrades InnoDB drops the FK's own index
36
+ // once the composite can serve the constraint. MySQL won't
37
+ // drop the last index a foreign key depends on, so re-add a
38
+ // single-column one when it objects.
39
+ if (err.code === 'ER_DROP_INDEX_FK') {
40
+ logging.warn('Cannot drop unique constraint over redirects(automation_action_revision_id, to_hash) while it backs the foreign key, re-adding index for automation_action_revision_id');
41
+
42
+ await addIndex('redirects', ['automation_action_revision_id'], knex);
43
+ await dropUnique('redirects', uniqueColumns, knex);
44
+ return;
45
+ }
46
+
47
+ throw err;
48
+ }
49
+ }
50
+ )
51
+ );
@@ -0,0 +1,5 @@
1
+ const {combineTransactionalMigrations, removeSetting} = require('../../utils');
2
+
3
+ module.exports = combineTransactionalMigrations(
4
+ removeSetting('routes_hash')
5
+ );
@@ -25,6 +25,11 @@ function addTableColumn(tableName, tableBuilder, columnName, columnSpec = schema
25
25
  // creation distinguishes between text with fieldtype, string with maxlength and all others
26
26
  if (columnSpec.type === 'text' && Object.prototype.hasOwnProperty.call(columnSpec, 'fieldtype')) {
27
27
  column = tableBuilder[columnSpec.type](columnName, columnSpec.fieldtype);
28
+ } else if (columnSpec.type === 'binary' && Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
29
+ // knex emits an unbounded `blob` for a length-less binary column, which
30
+ // MySQL can only index with a prefix. Passing the length gives us
31
+ // `varbinary(N)`, which is indexable in full.
32
+ column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
28
33
  } else if (columnSpec.type === 'string') {
29
34
  if (Object.prototype.hasOwnProperty.call(columnSpec, 'maxlength')) {
30
35
  column = tableBuilder[columnSpec.type](columnName, columnSpec.maxlength);
@@ -8,10 +8,6 @@
8
8
  "defaultValue": null,
9
9
  "type": "string"
10
10
  },
11
- "routes_hash": {
12
- "defaultValue": null,
13
- "type": "string"
14
- },
15
11
  "next_update_check": {
16
12
  "defaultValue": null,
17
13
  "type": "number"
@@ -1263,8 +1263,12 @@ module.exports = {
1263
1263
  to: {type: 'string', maxlength: 2000, nullable: false},
1264
1264
  post_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'posts.id', setNullDelete: true},
1265
1265
  automation_action_revision_id: {type: 'string', maxlength: 24, nullable: true, references: 'automation_action_revisions.id', setNullDelete: true},
1266
+ to_hash: {type: 'binary', maxlength: 32, nullable: true},
1266
1267
  created_at: {type: 'dateTime', nullable: false},
1267
- updated_at: {type: 'dateTime', nullable: true}
1268
+ updated_at: {type: 'dateTime', nullable: true},
1269
+ '@@UNIQUE_CONSTRAINTS@@': [
1270
+ ['automation_action_revision_id', 'to_hash']
1271
+ ]
1268
1272
  },
1269
1273
  members_click_events: {
1270
1274
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -17,29 +17,15 @@ const FETCH_ONLY_FORMATS = [
17
17
  ];
18
18
 
19
19
  class ImageSize {
20
- constructor({config, imageStore, storageUtils, validator, urlUtils, request, probe}) {
20
+ constructor({config, imageStore, storageUtils, validator, urlUtils, fetchExternal, probe}) {
21
21
  this.config = config;
22
22
  this.imageStore = imageStore;
23
23
  this.storageUtils = storageUtils;
24
24
  this.validator = validator;
25
25
  this.urlUtils = urlUtils;
26
- this.request = request;
26
+ this.fetchExternal = fetchExternal;
27
27
  this.probe = probe;
28
28
 
29
- this.REQUEST_OPTIONS = {
30
- // we need the user-agent, otherwise some https request may fail (e.g. cloudfare)
31
- headers: {
32
- 'User-Agent': 'Mozilla/5.0 Safari/537.36'
33
- },
34
- timeout: {
35
- request: this.config.get('times:getImageSizeTimeoutInMS') || 10000
36
- },
37
- retry: {
38
- limit: 0 // for `got`, used with image-size
39
- },
40
- responseType: 'buffer'
41
- };
42
-
43
29
  this.NEEDLE_OPTIONS = {
44
30
  // we need the user-agent, otherwise some https request may fail (e.g. cloudflare)
45
31
  headers: {
@@ -107,7 +93,7 @@ class ImageSize {
107
93
  // download full image then use image-size to get it's dimensions
108
94
  // returns promise which resolves dimensions
109
95
  _fetchImageSizeFromUrl(imageUrl) {
110
- return this.request(imageUrl, this.REQUEST_OPTIONS).then((response) => {
96
+ return this.fetchExternal(imageUrl, this.NEEDLE_OPTIONS).then((response) => {
111
97
  return this._imageSizeFromBuffer(response.body);
112
98
  });
113
99
  }
@@ -24,10 +24,23 @@ function probe(url, options = {}, {probeImageSize: probeImageSizeFn = probeImage
24
24
  return promise;
25
25
  }
26
26
 
27
+ // For image formats that require a full fetch. This function ensures that
28
+ // SSRF protections are in place by using the externalRequest `got` client.
29
+ function fetchExternal(url, options = {}) {
30
+ return externalRequest.get(url, {
31
+ headers: options.headers,
32
+ timeout: {
33
+ request: options.response_timeout || 10000
34
+ },
35
+ responseType: 'buffer',
36
+ retry: {limit: 0}
37
+ });
38
+ }
39
+
27
40
  class ImageUtils {
28
41
  constructor({config, urlUtils, settingsCache, storageUtils, imageStore, validator, request, cacheStore}) {
29
42
  this.blogIcon = new BlogIcon({config, urlUtils, settingsCache, storageUtils});
30
- this.imageSize = new ImageSize({config, imageStore, storageUtils, validator, urlUtils, request, probe});
43
+ this.imageSize = new ImageSize({config, imageStore, storageUtils, validator, urlUtils, fetchExternal, probe});
31
44
  this.cachedImageSizeFromUrl = new CachedImageSizeFromUrl({
32
45
  getImageSizeFromUrl: this.imageSize.getImageSizeFromUrl.bind(this.imageSize),
33
46
  cache: cacheStore
@@ -38,3 +51,4 @@ class ImageUtils {
38
51
 
39
52
  module.exports = ImageUtils;
40
53
  module.exports.probe = probe;
54
+ module.exports.fetchExternal = fetchExternal;
@@ -248,7 +248,7 @@ function installSafeDnsLookup(options) {
248
248
  const requestHref = options.url.href;
249
249
  // Use 'lookup' (the native http.request option) rather than 'dnsLookup'
250
250
  // (got's public API property which doesn't flow to the native request).
251
- options.lookup = (hostname, dnsOpts, callback) => {
251
+ options.dnsLookup = (hostname, dnsOpts, callback) => {
252
252
  if (typeof dnsOpts === 'function') {
253
253
  callback = dnsOpts;
254
254
  dnsOpts = {};