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.
- package/components/ghost-storage-base-3.0.0.tgz +0 -0
- package/components/tryghost-adapter-base-cache-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-adapter-base-sso-0.1.1.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-sjHLx5dy.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-DoYXtnZ8.js} +4 -4
- package/core/built/admin/assets/activitypub/activitypub.js +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-zfxUey_L.js → bluesky-sharing-Bxeh6MKW.js} +4 -4
- package/core/built/admin/assets/activitypub/{dist-BqkDUQxJ2.js → dist-ByFmDSNP2.js} +64 -63
- package/core/built/admin/assets/activitypub/{dist-Ba_QxHHl.js → dist-D8sFfdpC.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-CWueFW6q.js} +5 -5
- package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BGxPU7N5.js} +3 -3
- package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-DBK-u0Ch.js} +3 -3
- package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-Bcir5RQZ.js} +3 -3
- package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-BV-CIT3d.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-yC0nfvzl.js} +4 -4
- package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-DTRS9TDk.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-Dwl9t0pb.js} +4 -4
- package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-B-zKsZ7r.js} +3 -3
- package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-DjJZD54t.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-CoU8vs6d.js} +6 -6
- package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-BAsaJekx.js} +6 -6
- package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CLH3OVTP.js} +28 -25
- package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-DA6pNJ-Q.js} +3 -3
- package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-CQD42Pw5.js} +2 -2
- package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-Cg0saow1.js} +7 -7
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-Br4aOWOh.js} +2 -2
- package/core/built/admin/assets/audience-DjEWNhp3.js +1 -0
- package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-DRy7qY-J.js} +2 -2
- package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-Bx3VcFIL.js} +1 -1
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-ClMd0-Ft.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-BD0U2ygg.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-fjQKRVyY.js} +1 -1
- package/core/built/admin/assets/checkbox-CWa28lgO.js +1 -0
- package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js → chunk.211.6f7328d63c41536451b3.js} +737 -559
- package/core/built/admin/assets/{chunk.962.b73442b07be930bb5d85.js.LICENSE.txt → chunk.211.6f7328d63c41536451b3.js.LICENSE.txt} +2 -0
- package/core/built/admin/assets/{chunk.589.096f4d0bb775e5229aa9.js → chunk.481.6c7395a49484bb4583aa.js} +41 -38
- package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +25 -0
- package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +27 -0
- package/core/built/admin/assets/code-editor-view-CCr0rBWS.js +1 -0
- package/core/built/admin/assets/comments-DyUSMUdj.js +1 -0
- package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-BFp58EMk.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-CUxMfiPa.js} +1 -1
- package/core/built/admin/assets/dist-2Tnz2Yfq.js +1 -0
- package/core/built/admin/assets/dist-CjY-f5Z1.js +1 -0
- package/core/built/admin/assets/dist-DIC8yz5C.js +1 -0
- package/core/built/admin/assets/domain-C0bV2UV8.js +1 -0
- package/core/built/admin/assets/dropzone-DlK-K2DQ.js +5 -0
- package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-BKT-REJw.js} +1 -1
- package/core/built/admin/assets/editor-Dp865O-4.js +8 -0
- package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-ClUpJZOp.js} +1 -1
- package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-DF92dUMP.js} +1 -1
- package/core/built/admin/assets/field-Do7lieBA.js +1 -0
- package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-BV2rnpfA.js} +1 -1
- package/core/built/admin/assets/filters-CUXJ2Nm9.js +1 -0
- package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-DfbIWc_l.js} +1 -1
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css} +1 -1
- package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-c4057474943effc568c5659ccc4909d7.js} +407 -406
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-db36d795d21c61426837df40af270966.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-f9VIMNDv.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-B3I6gNLm.js +1 -0
- package/core/built/admin/assets/{Growth-CSaHoSVD.js → growth-CVw1My4S.js} +2 -2
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-CFBSu4Ed.js} +12 -12
- package/core/built/admin/assets/index-9Cw8z3rA.css +1 -0
- package/core/built/admin/assets/index-BGG7NckO.js +67 -0
- package/core/built/admin/assets/input-group-BSgG_lVw.js +1 -0
- package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +974 -937
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +28 -28
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-f8vHRYvz.js} +28 -28
- package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DSap9rAy.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-DU0rn9Cj.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-Dz33AV6h.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-BtTTrKvJ.js} +1 -1
- package/core/built/admin/assets/{label-BOR9XdKl.js → label-DS8Mi6G5.js} +1 -1
- package/core/built/admin/assets/label-picker-1DvSuLA1.js +1 -0
- package/core/built/admin/assets/labels-CDSUt4J_.js +1 -0
- package/core/built/admin/assets/layout-yQEi2qDM.js +1 -0
- package/core/built/admin/assets/{link-helpers-tOlxmuH5.js → link-helpers-7EYqvvHs.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +1 -0
- package/core/built/admin/assets/member-detail-BAq7gDaa.js +1 -0
- package/core/built/admin/assets/members-CQpVFJu4.js +10 -0
- package/core/built/admin/assets/modals-ReORvWnu.js +31 -0
- package/core/built/admin/assets/moderation-DySbgyUS.js +1 -0
- package/core/built/admin/assets/newsletter-CJQvtHqN.js +1 -0
- package/core/built/admin/assets/{Newsletters-C5cs6FGo.js → newsletters-B3QQXfGT.js} +1 -1
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/note-C8kxrz6w.js +1 -0
- package/core/built/admin/assets/notifications-CgtSfX7t.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-4qtziA9d.js +1 -0
- package/core/built/admin/assets/overview-Bf2sbznq.js +1 -0
- package/core/built/admin/assets/overview-CBfePxcr.js +1 -0
- package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-B3Wb9Jqv.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-We-ko5Cb.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-B6UynTGb.js +1 -0
- package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-DFfO93_0.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/{preferences-D4EANGoc.js → preferences-DJPfHEGu.js} +2 -2
- package/core/built/admin/assets/profile-DqBQp-gT.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/select-C5_xEt5b.js +1 -0
- package/core/built/admin/assets/settings-BwiVIXau.js +4 -0
- package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-WXNeWyU1.js} +1 -1
- package/core/built/admin/assets/shade-app-B3heOAfz-CGXQwWeO.js +3 -0
- package/core/built/admin/assets/{share-modal-CkdTTMJd.js → share-modal-DsgW3wbg.js} +1 -1
- package/core/built/admin/assets/social-urls-BQPFEk1_.js +8 -0
- package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-Bb_FCl7e.js} +1 -1
- package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-BeaRxUn9.js} +1 -1
- package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-Bjj_kmJ1.js} +1 -1
- package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-BxPbW6K1.js} +1 -1
- package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-B1e3-1F8.js} +1 -1
- package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-C5WaeBJo.js} +1 -1
- package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-B46iVPwu.js} +2 -2
- package/core/built/admin/assets/switch-z5Y6Uzt6.js +1 -0
- package/core/built/admin/assets/{table-B8LgTQJZ.js → table-CB-4_HBJ.js} +1 -1
- package/core/built/admin/assets/tabs-CCEgKYzq.js +1 -0
- package/core/built/admin/assets/{tags-B4l891z2.js → tags-Cg2VJGmG.js} +1 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-b3IlxvhV.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-DAFT87tQ.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-S0FTd43s.js} +1 -1
- package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-CIaiHOVb.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-Cq2vBPrA.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DBDRtGu-.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-CjYNxdwe.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-falHAtuK.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-D7-8ND1y.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-CvnC9zF2.js} +1 -1
- package/core/built/admin/assets/web-B_5GZRvH.js +1 -0
- package/core/built/admin/assets/web-jFYWCKDn.js +2 -0
- package/core/built/admin/index.html +14 -10
- package/core/frontend/services/proxy.js +10 -1
- package/core/frontend/services/routing/router-manager.js +14 -14
- package/core/frontend/services/routing/static-routes-router.js +8 -10
- package/core/server/adapters/storage/LocalFilesStorage.js +13 -11
- package/core/server/adapters/storage/LocalFilesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +14 -12
- package/core/server/adapters/storage/LocalImagesStorage.ts +24 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +13 -11
- package/core/server/adapters/storage/LocalMediaStorage.ts +24 -0
- package/core/server/adapters/storage/LocalStorageBase.js +107 -167
- package/core/server/adapters/storage/LocalStorageBase.ts +342 -0
- package/core/server/adapters/storage/utils.js +36 -50
- package/core/server/adapters/storage/utils.ts +60 -0
- package/core/server/api/endpoints/automation-action-links.js +68 -0
- package/core/server/api/endpoints/automation-action-links.ts +46 -0
- package/core/server/api/endpoints/db.js +6 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/members.js +25 -28
- package/core/server/api/endpoints/pages.js +6 -1
- package/core/server/api/endpoints/posts.js +11 -2
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +4 -1
- package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- package/core/server/data/exporter/export-filename.js +2 -1
- package/core/server/data/importer/importers/data/revue-subscriber-importer.js +6 -10
- package/core/server/data/importer/importers/json-to-html.js +24 -4
- package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
- package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
- package/core/server/data/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/schema.js +5 -1
- package/core/server/lib/image/image-size.js +3 -17
- package/core/server/lib/image/image-utils.js +15 -1
- package/core/server/lib/request-external.js +1 -1
- package/core/server/models/user.js +46 -70
- package/core/server/services/auth/session/session-service.js +28 -1
- package/core/server/services/automations/automations-api.js +21 -6
- package/core/server/services/automations/automations-api.ts +25 -6
- package/core/server/services/automations/automations-repository.ts +19 -0
- package/core/server/services/automations/database-automations-repository.js +116 -28
- package/core/server/services/automations/database-automations-repository.ts +118 -9
- package/core/server/services/automations/database-date.js +10 -0
- package/core/server/services/automations/database-date.ts +5 -0
- package/core/server/services/automations/poll.js +5 -2
- package/core/server/services/automations/poll.ts +7 -2
- package/core/server/services/automations/scheduler-poll-time.js +40 -0
- package/core/server/services/automations/scheduler-poll-time.ts +38 -0
- package/core/server/services/automations/service.js +15 -3
- package/core/server/services/automations/service.ts +16 -3
- package/core/server/services/comments/comments-service-emails.js +14 -8
- package/core/server/services/comments/comments-service.js +9 -4
- package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
- package/core/server/services/email-service/email-service-wrapper.js +6 -1
- package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
- package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
- package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
- package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
- package/core/server/services/link-redirection/link-redirect.js +2 -0
- package/core/server/services/link-redirection/link-redirect.ts +3 -1
- package/core/server/services/link-redirection/link-redirects-service.js +38 -2
- package/core/server/services/link-tracking/click-event.js +3 -0
- package/core/server/services/link-tracking/index.js +38 -6
- package/core/server/services/link-tracking/link-click-repository.js +30 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +44 -3
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +19 -4
- package/core/server/services/member-welcome-emails/service.js +11 -3
- package/core/server/services/members/{importer → import-export}/config.js +4 -9
- package/core/server/services/members/{importer → import-export}/config.ts +4 -9
- package/core/server/services/members/import-export/csv/formula.js +16 -0
- package/core/server/services/members/import-export/csv/formula.ts +14 -0
- package/core/server/services/members/import-export/csv/index.js +12 -0
- package/core/server/services/members/import-export/csv/index.ts +3 -0
- package/core/server/services/members/import-export/csv/parse.js +64 -0
- package/core/server/services/members/import-export/csv/parse.ts +69 -0
- package/core/server/services/members/import-export/csv/serialize.js +13 -0
- package/core/server/services/members/import-export/csv/serialize.ts +18 -0
- package/core/server/services/members/import-export/export/exporter.js +202 -0
- package/core/server/services/members/import-export/export/exporter.ts +325 -0
- package/core/server/services/members/import-export/import/completion-email.js +84 -0
- package/core/server/services/members/import-export/import/completion-email.ts +141 -0
- package/core/server/services/members/import-export/import/importer.js +297 -0
- package/core/server/services/members/import-export/import/importer.ts +468 -0
- package/core/server/services/members/import-export/import/reader.js +32 -0
- package/core/server/services/members/import-export/import/reader.ts +33 -0
- package/core/server/services/members/import-export/import/row.js +33 -0
- package/core/server/services/members/import-export/import/row.ts +52 -0
- package/core/server/services/members/import-export/import/spool.js +30 -0
- package/core/server/services/members/import-export/import/spool.ts +38 -0
- package/core/server/services/members/import-export/index.js +99 -0
- package/core/server/services/members/import-export/index.ts +137 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
- package/core/server/services/members/service.js +25 -20
- package/core/server/services/members-custom-fields/values-service.js +19 -7
- package/core/server/services/members-custom-fields/values-service.ts +19 -7
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/posts/posts-exporter.js +6 -4
- package/core/server/services/posts/posts-service.js +2 -2
- package/core/server/services/route-settings/activation-bridge.js +59 -49
- package/core/server/services/route-settings/activation-bridge.ts +91 -51
- package/core/server/services/route-settings/dynamic-routing-service.js +22 -51
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/themes/storage.js +9 -0
- package/core/server/services/themes/theme-storage.js +1 -1
- package/core/server/services/url/url-service-facade.js +1 -1
- package/core/server/services/url/url-service-facade.ts +8 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/shared/config/defaults.json +2 -1
- package/core/shared/express.js +6 -1
- package/core/shared/labs.js +2 -1
- package/package.json +11 -11
- package/pnpm-lock.yaml +18 -18
- package/pnpm-workspace.yaml +8 -7
- package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
- package/core/built/admin/assets/Overview-j4NaeUNm.js +0 -1
- package/core/built/admin/assets/Web-_fD935OX.js +0 -2
- package/core/built/admin/assets/activitypub/layout-IdVq3lRy.js +0 -1
- package/core/built/admin/assets/audience-BKRdsFmy.js +0 -1
- package/core/built/admin/assets/checkbox-DA4-1UiI.js +0 -1
- package/core/built/admin/assets/chunk.524.45e0875f41b71e46e849.js +0 -25
- package/core/built/admin/assets/chunk.582.51defd4b009f0de8d4d5.js +0 -27
- package/core/built/admin/assets/code-editor-view-BKBrkffZ.js +0 -1
- package/core/built/admin/assets/comments-CzVNIT8b.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CT46_f_J.js +0 -1
- package/core/built/admin/assets/domain-B7_n5nfx.js +0 -1
- package/core/built/admin/assets/dropzone-DcuJEEUf.js +0 -5
- package/core/built/admin/assets/editor-BCiIyFjm.js +0 -8
- package/core/built/admin/assets/field-BW-M2Zwp.js +0 -1
- package/core/built/admin/assets/filters-DtcXMZDU.js +0 -1
- package/core/built/admin/assets/gift-link-modal-Bk8qpnMw.js +0 -1
- package/core/built/admin/assets/growth-Ck2s3JjR.js +0 -1
- package/core/built/admin/assets/index-BBf502Wg.js +0 -69
- package/core/built/admin/assets/index-BCCZKxP2.css +0 -1
- package/core/built/admin/assets/input-group-DjuYnNCd.js +0 -1
- package/core/built/admin/assets/label-picker-BdNAWuUX.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-Bw4GnxXp.js +0 -1
- package/core/built/admin/assets/member-custom-fields-DvFyamLu.js +0 -1
- package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
- package/core/built/admin/assets/members-DsPxYAVm.js +0 -10
- package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
- package/core/built/admin/assets/moderation-Dlp7M5sa.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-CH1o8XXQ.js +0 -1
- package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
- package/core/built/admin/assets/note-BrAZ7Z0o.js +0 -1
- package/core/built/admin/assets/notifications-BnbHLOPC.js +0 -1
- package/core/built/admin/assets/onboarding-route-eU146kuA.js +0 -1
- package/core/built/admin/assets/overview-BpbaK-xu.js +0 -1
- package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BDqcfBor.js +0 -1
- package/core/built/admin/assets/profile-dJ44Bn_W.js +0 -1
- package/core/built/admin/assets/select--2uVN9_-.js +0 -1
- package/core/built/admin/assets/settings-SD9bxIpt.js +0 -4
- package/core/built/admin/assets/social-urls-BvFZwYcE.js +0 -8
- package/core/built/admin/assets/switch-qp518gFQ.js +0 -1
- package/core/built/admin/assets/tabs-D3ZOYkM7.js +0 -1
- package/core/built/admin/assets/textarea-CTlb3Uu6.js +0 -1
- package/core/built/admin/assets/use-filter-params-DeQ8sX4N.js +0 -1
- package/core/built/admin/assets/web-DTZnnUGr.js +0 -1
- package/core/server/services/mail/templates/newsletter.html +0 -876
- package/core/server/services/members/csv/index.js +0 -10
- package/core/server/services/members/csv/index.ts +0 -3
- package/core/server/services/members/csv/parse.js +0 -94
- package/core/server/services/members/csv/parse.ts +0 -111
- package/core/server/services/members/csv/types.js +0 -2
- package/core/server/services/members/csv/types.ts +0 -39
- package/core/server/services/members/csv/unparse.js +0 -69
- package/core/server/services/members/csv/unparse.ts +0 -79
- package/core/server/services/members/exporter/query.js +0 -233
- package/core/server/services/members/importer/index.js +0 -30
- package/core/server/services/members/importer/labels.js +0 -125
- package/core/server/services/members/importer/members-csv-importer.js +0 -520
- package/core/server/services/route-settings/default-settings-manager.js +0 -63
- package/core/server/services/route-settings/route-settings.js +0 -170
- package/core/server/services/route-settings/settings-loader.js +0 -60
- package/core/server/services/route-settings/settings-path-manager.js +0 -47
- package/core/server/services/route-settings/validate.js +0 -440
- /package/core/server/services/members/{importer → import-export/import}/email-template.js +0 -0
- /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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
406
|
+
* @param {{data: NodeJS.ReadableStream, filename?: string}} data
|
|
469
407
|
*
|
|
470
|
-
* @returns {
|
|
408
|
+
* @returns {Function} - A response handler that streams the CSV
|
|
471
409
|
*/
|
|
472
410
|
function exportCSV(data) {
|
|
473
411
|
debug('exportCSV');
|
|
474
412
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
58
|
+
href: itemURL
|
|
39
59
|
}
|
|
40
60
|
};
|
|
41
61
|
itemHTMLChunks.push(serializer.serialize(imageCard.render(cardOpts)));
|
|
42
|
-
|
|
43
|
-
let linkHTML = `<h4
|
|
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') {
|
package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js
ADDED
|
@@ -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
|
+
);
|
|
@@ -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);
|
|
@@ -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,
|
|
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.
|
|
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.
|
|
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,
|
|
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.
|
|
251
|
+
options.dnsLookup = (hostname, dnsOpts, callback) => {
|
|
252
252
|
if (typeof dnsOpts === 'function') {
|
|
253
253
|
callback = dnsOpts;
|
|
254
254
|
dnsOpts = {};
|