ghost 6.54.0 → 6.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +3 -0
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +1 -0
- package/core/built/admin/assets/Overview-BjACEsJF.js +1 -0
- package/core/built/admin/assets/Web-CxxxjCcv.js +2 -0
- package/core/built/admin/assets/{account-migration-DLvVA5yq.js → account-migration-BklF2I_I.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DHbZcxGM.js → account-migration-CNovr6W-.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-Ceslb1_M.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-DHAjBcV3.js} +2 -2
- package/core/built/admin/assets/activitypub/{domain-CmwSZLHK.js → domain-C1qQvJ8C.js} +5 -5
- package/core/built/admin/assets/activitypub/{edit-profile-CXdwNmVG.js → edit-profile-BcH-jEfM.js} +3 -3
- package/core/built/admin/assets/activitypub/{explore-DfbjzGM1.js → explore-AxwBh9dI.js} +3 -3
- package/core/built/admin/assets/activitypub/{feed-DSEBJOqf.js → feed-BaOlbh7a.js} +3 -3
- package/core/built/admin/assets/activitypub/{field-CY7XVcx6.js → field-B7mYYQpN.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DtQ0kytT.js → inbox-vXez-5uJ.js} +4 -4
- package/core/built/admin/assets/activitypub/{label-Dw96GQ7u.js → label-Chv5T8bw.js} +2 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dg_-I-8C.js → moderation-DtSM2i1I.js} +4 -4
- package/core/built/admin/assets/activitypub/{note-Cy1-uKMY.js → note-Dckp4S-2.js} +3 -3
- package/core/built/admin/assets/activitypub/{notifications-B_fvoLIM.js → notifications-CG5T22vg.js} +3 -3
- package/core/built/admin/assets/activitypub/{preferences-ByJseZp6.js → preferences-D_799jdl.js} +6 -6
- package/core/built/admin/assets/activitypub/{profile-BFtbnIjz.js → profile-D2JfXBQl.js} +6 -6
- package/core/built/admin/assets/activitypub/{routes-Dh3xXfce.js → routes-CCHe2dQp.js} +28 -25
- package/core/built/admin/assets/activitypub/{settings-DTgqJPxM.js → settings-kx-9Enkz.js} +3 -3
- package/core/built/admin/assets/activitypub/{site-CdL9ZLqm.js → site-x4_IjVJ0.js} +2 -2
- package/core/built/admin/assets/activitypub/{tabs-ogqwoBlb.js → tabs-CyPRQQ1N.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-CxonteFD.js → use-reply-chain-data-BAB6fNGt.js} +2 -2
- package/core/built/admin/assets/{audience-BKRdsFmy.js → audience-X_XsZivI.js} +1 -1
- package/core/built/admin/assets/{automated-emails-Cn87L5Md.js → automated-emails-of1MvVsv.js} +1 -1
- package/core/built/admin/assets/{automations-BP_aHamJ.js → automations-DRFjW5fw.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-Bh36XPh4.js → bluesky-sharing-BbbjWdzs.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-D34YWnaw.js → breadcrumb-B-M9Ip_2.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-DbklaJw9.js → chart-helpers-xiSsW8r0.js} +1 -1
- package/core/built/admin/assets/checkbox-EhJKHFdZ.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.361.3cdb584b973959e52ccc.js} +3 -3
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +25 -0
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +27 -0
- package/core/built/admin/assets/code-editor-view-CZtJFGhR.js +1 -0
- package/core/built/admin/assets/comments-CvyXxOyN.js +1 -0
- package/core/built/admin/assets/{date-range-select-B5vSsrpx.js → date-range-select-CTlyo8zW.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-NkOIoMue.js → disabled-sources-indicator-qs2GfLvl.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-B7_n5nfx.js → domain-CPNXKVOV.js} +1 -1
- package/core/built/admin/assets/dropzone-BmCXnw6J.js +5 -0
- package/core/built/admin/assets/{edit-profile-rvDGrS8J.js → edit-profile-SGrJve5M.js} +1 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +8 -0
- package/core/built/admin/assets/{explore-BZAZOg5X.js → explore-CUYLv9G9.js} +1 -1
- package/core/built/admin/assets/{feed-CFV8xDw9.js → feed-CPdJMttL.js} +1 -1
- package/core/built/admin/assets/{field-BW-M2Zwp.js → field-D0XISCK2.js} +1 -1
- package/core/built/admin/assets/{filter-sources-Fx8wiYYw.js → filter-sources-Bieps75g.js} +1 -1
- package/core/built/admin/assets/{filters-CGX_pf2N.js → filters-CuB-A7ma.js} +1 -1
- package/core/built/admin/assets/filters-bXkkWro8.js +1 -0
- package/core/built/admin/assets/{ghost-b1dce91ad3232cc259ee5fbbf9526243.js → ghost-05f9a7182ed56903dba05878a5b8422b.js} +290 -289
- package/core/built/admin/assets/{gift-link-modal-Bk8qpnMw.js → gift-link-modal-su8MsRdg.js} +1 -1
- package/core/built/admin/assets/{growth-Ck2s3JjR.js → growth-Ba6S5FQX.js} +1 -1
- package/core/built/admin/assets/{inbox-BRp1_aaC.js → inbox-Xx1CdnfO.js} +8 -8
- package/core/built/admin/assets/index-D8PbzuT9.css +1 -0
- package/core/built/admin/assets/index-ukW_0Uie.js +67 -0
- package/core/built/admin/assets/input-group-CGOXcy4l.js +1 -0
- package/core/built/admin/assets/input-surface-BpAjZXY5.js +1 -0
- package/core/built/admin/assets/{koenig-lexical-CM7UFNdL.js → koenig-lexical-B61IALf5.js} +1 -1
- package/core/built/admin/assets/{kpi-card-CIGyijjx.js → kpi-card-BlZaG1Lo.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BrGH6FsT.js → kpi-card-DCdlyaSS.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-_HRRsklE.js → kpi-helpers-DpAuS1Nb.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BSvh9eoq.js → kpi-tabs-Dme44bQS.js} +1 -1
- package/core/built/admin/assets/{label-BOR9XdKl.js → label-dSs5aepE.js} +1 -1
- package/core/built/admin/assets/{label-picker-BdNAWuUX.js → label-picker-Uq91atex.js} +1 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +1 -0
- package/core/built/admin/assets/{member-custom-fields-DvFyamLu.js → member-custom-fields-NCzrMuba.js} +1 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +1 -0
- package/core/built/admin/assets/{members-DsPxYAVm.js → members-B9rO7qr4.js} +2 -2
- package/core/built/admin/assets/modals-CxRaan1J.js +31 -0
- package/core/built/admin/assets/{moderation-Dlp7M5sa.js → moderation-D8zon60E.js} +1 -1
- package/core/built/admin/assets/{multi-select-combobox-CH1o8XXQ.js → multi-select-combobox-1wYm3nle.js} +1 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +1 -0
- package/core/built/admin/assets/{note-BrAZ7Z0o.js → note-PiWPnnrh.js} +1 -1
- package/core/built/admin/assets/{notifications-BnbHLOPC.js → notifications-DEgqqB9G.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-eU146kuA.js → onboarding-route-BzF6HeVn.js} +1 -1
- package/core/built/admin/assets/{overview-BpbaK-xu.js → overview-DixLeIqY.js} +1 -1
- package/core/built/admin/assets/{page-header-C0jVxtS8.js → page-header-op0eN36I.js} +1 -1
- package/core/built/admin/assets/{post-analytics-Iw7dk1mT.js → post-analytics-WwRDEnrV.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +1 -0
- package/core/built/admin/assets/{post-analytics-provider-BDqcfBor.js → post-analytics-provider-BJCPLMIb.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-DBfKl2GZ.js → post-share-modal-Dx-MEjhI.js} +1 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +5 -0
- package/core/built/admin/assets/profile-Dl69QPp0.js +1 -0
- package/core/built/admin/assets/select-C1qDZnW4.js +1 -0
- package/core/built/admin/assets/settings-BbmC_Iqq.js +4 -0
- package/core/built/admin/assets/{settings-Bz3RZRmE.js → settings-H6HxFgfI.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-BJCadJyp.js} +1 -1
- package/core/built/admin/assets/social-urls-BPL1nADp.js +8 -0
- package/core/built/admin/assets/{sort-button-DZp9Y2r1.js → sort-button-DyYlfYp7.js} +1 -1
- package/core/built/admin/assets/{source-icon-CHr9qJ2Y.js → source-icon-_ubRDbg_.js} +1 -1
- package/core/built/admin/assets/{stats-DGoXZxWE.js → stats-BQZMGMvn.js} +1 -1
- package/core/built/admin/assets/{stats-view-D2-1L0ms.js → stats-view-Doei-qCX.js} +1 -1
- package/core/built/admin/assets/{step-1-RdahbwyG.js → step-1-IKiLBPoC.js} +1 -1
- package/core/built/admin/assets/{step-2-D83PeRwd.js → step-2-jJ41PsDN.js} +1 -1
- package/core/built/admin/assets/{step-3-p_Op4wv4.js → step-3-CaqFAEqf.js} +2 -2
- package/core/built/admin/assets/switch-t-WyENU2.js +1 -0
- package/core/built/admin/assets/{table-B8LgTQJZ.js → table-DxUL78jU.js} +1 -1
- package/core/built/admin/assets/{tabs-D3ZOYkM7.js → tabs-BQcJQ1ke.js} +1 -1
- package/core/built/admin/assets/{tags-B4l891z2.js → tags-CNujGwkT.js} +1 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +1 -0
- package/core/built/admin/assets/{toggle-group-DNXDkAwf.js → toggle-group-BeEpQ69p.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DZGjEcz3.js → topic-filter-Bb5MOD-1.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-CaT8RirX.js → use-analytics-data-Cds6XDjg.js} +1 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-9Db2CuJe.js → use-growth-stats-D8uf98-G.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-BV54YcQS.js → use-post-referrers-CRtgyuXB.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DuYZGDan.js → use-reply-chain-data-DbKYX50v.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-8fTua9Nt.js → use-simple-pagination-DC01kpo0.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-Dz156HvZ.js → use-tinybird-query-C7bQRJWw.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-C9RUdL_n.js → use-tinybird-token-Az4tam7_.js} +1 -1
- package/core/built/admin/assets/{virtual-list-BCvP0YF-.js → virtual-list-Dhuh9fGx.js} +1 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +1 -0
- package/core/built/admin/index.html +11 -7
- package/core/frontend/services/proxy.js +11 -1
- 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/db.js +6 -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/utils/serializers/output/mappers/comments.js +4 -1
- package/core/server/api/endpoints/utils/serializers/output/members.js +18 -87
- 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/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 +6 -6
- package/core/server/services/automations/automations-api.ts +6 -6
- package/core/server/services/automations/automations-repository.ts +1 -0
- package/core/server/services/automations/database-automations-repository.js +7 -2
- package/core/server/services/automations/database-automations-repository.ts +8 -2
- package/core/server/services/comments/comments-service-emails.js +4 -1
- 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/{csv → import-export/csv}/index.js +3 -3
- package/core/server/services/members/import-export/csv/index.ts +2 -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 +65 -0
- package/core/server/services/members/import-export/import/completion-email.ts +119 -0
- package/core/server/services/members/import-export/import/importer.js +276 -0
- package/core/server/services/members/import-export/import/importer.ts +430 -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 +90 -0
- package/core/server/services/members/import-export/index.ts +118 -0
- package/core/server/services/members/members-api/controllers/router-controller.js +7 -0
- package/core/server/services/members/service.js +17 -19
- package/core/server/services/posts/posts-exporter.js +6 -4
- package/core/server/services/posts/posts-service.js +2 -2
- package/core/server/services/themes/storage.js +9 -0
- package/core/server/services/themes/theme-storage.js +1 -1
- 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 +9 -9
- package/pnpm-lock.yaml +17 -17
- package/pnpm-workspace.yaml +7 -7
- package/components/tryghost-adapter-base-scheduling-0.1.1.tgz +0 -0
- package/core/built/admin/assets/Growth-CSaHoSVD.js +0 -3
- package/core/built/admin/assets/Newsletters-C5cs6FGo.js +0 -1
- 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/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/dist-CT46_f_J.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/filters-DtcXMZDU.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/layout-Bw4GnxXp.js +0 -1
- package/core/built/admin/assets/member-detail-Cg2vTqvJ.js +0 -1
- package/core/built/admin/assets/modals-Bf_fyvHZ.js +0 -31
- package/core/built/admin/assets/newsletter-CQ5CAYUI.js +0 -1
- package/core/built/admin/assets/post-analytics-header-D9pJ4iNm.js +0 -1
- package/core/built/admin/assets/preferences-D4EANGoc.js +0 -5
- 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/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.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/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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// # Local File Base Storage module
|
|
2
|
+
// The (default) module for storing files using the local file system
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import type {NextFunction, Request, RequestHandler, Response} from 'express';
|
|
6
|
+
import type express from 'express';
|
|
7
|
+
import tpl from '@tryghost/tpl';
|
|
8
|
+
import errors from '@tryghost/errors';
|
|
9
|
+
import {StorageBase, type ReadOptions, type StorageFile} from 'ghost-storage-base';
|
|
10
|
+
import urlUtils from '../../../shared/url-utils';
|
|
11
|
+
import {errify} from '../../../shared/errify';
|
|
12
|
+
|
|
13
|
+
const serveStatic: typeof express.static = require('../../../shared/express').static;
|
|
14
|
+
|
|
15
|
+
const messages = {
|
|
16
|
+
notFound: 'File not found',
|
|
17
|
+
notFoundWithRef: 'File not found: {file}',
|
|
18
|
+
cannotRead: 'Could not read file: {file}',
|
|
19
|
+
invalidUrlParameter: `The URL "{url}" is not a valid URL for this site.`,
|
|
20
|
+
invalidPathParameter: 'The path "{path}" is not valid for this storage.'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface LocalStorageBaseErrorMessages {
|
|
24
|
+
notFound: string;
|
|
25
|
+
notFoundWithRef: string;
|
|
26
|
+
cannotRead: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LocalStorageBaseOptions {
|
|
30
|
+
storagePath: string;
|
|
31
|
+
siteUrl?: string;
|
|
32
|
+
staticFileURLPrefix?: string;
|
|
33
|
+
errorMessages?: LocalStorageBaseErrorMessages;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Errors thrown by `serve-static` carry an HTTP status alongside the standard
|
|
38
|
+
* Error fields.
|
|
39
|
+
*/
|
|
40
|
+
interface ServeStaticError extends Error {
|
|
41
|
+
statusCode?: number;
|
|
42
|
+
path?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class LocalStorageBase extends StorageBase {
|
|
46
|
+
readonly staticFileURLPrefix: string | undefined;
|
|
47
|
+
|
|
48
|
+
readonly siteUrl: string | undefined;
|
|
49
|
+
|
|
50
|
+
readonly staticFileUrl: string;
|
|
51
|
+
|
|
52
|
+
readonly errorMessages: LocalStorageBaseErrorMessages;
|
|
53
|
+
|
|
54
|
+
constructor({storagePath, staticFileURLPrefix, siteUrl, errorMessages}: LocalStorageBaseOptions) {
|
|
55
|
+
super();
|
|
56
|
+
|
|
57
|
+
this.storagePath = storagePath;
|
|
58
|
+
this.staticFileURLPrefix = staticFileURLPrefix;
|
|
59
|
+
this.siteUrl = siteUrl;
|
|
60
|
+
this.staticFileUrl = `${siteUrl}${staticFileURLPrefix}`;
|
|
61
|
+
this.errorMessages = errorMessages || messages;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Normalizes a relative storage path and rejects traversal outside the storage root.
|
|
66
|
+
*/
|
|
67
|
+
_normalizeStorageRelativePath(filePath?: string): string {
|
|
68
|
+
const normalized = path.posix.normalize(String(filePath || '')
|
|
69
|
+
.replaceAll('\\', '/')
|
|
70
|
+
.replace(/^\/+/, '')
|
|
71
|
+
.replace(/\/+$/, ''));
|
|
72
|
+
|
|
73
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
74
|
+
throw new errors.IncorrectUsageError({
|
|
75
|
+
message: tpl(messages.invalidPathParameter, {path: filePath})
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves a target directory and optional file name into a full path,
|
|
84
|
+
* validating the result is inside the storage root.
|
|
85
|
+
*
|
|
86
|
+
* Supports relative paths (preferred) and absolute paths (legacy).
|
|
87
|
+
* TODO: remove absolute path support once all callers pass relative paths
|
|
88
|
+
*
|
|
89
|
+
* @param targetDir absolute or relative directory
|
|
90
|
+
* @param fileName file name to normalize and append
|
|
91
|
+
* @returns resolved absolute path inside storagePath
|
|
92
|
+
*/
|
|
93
|
+
_resolveAndValidateStoragePath(targetDir?: string, fileName?: string): string {
|
|
94
|
+
const resolvedRoot = path.resolve(this.storagePath);
|
|
95
|
+
|
|
96
|
+
// Resolve targetDir: if already inside storage root use as-is, otherwise treat as relative
|
|
97
|
+
let resolvedBase;
|
|
98
|
+
if (targetDir) {
|
|
99
|
+
const resolvedTargetDir = path.resolve(targetDir);
|
|
100
|
+
const relToRoot = path.relative(resolvedRoot, resolvedTargetDir);
|
|
101
|
+
if (relToRoot === '' || (!relToRoot.startsWith('..') && !path.isAbsolute(relToRoot))) {
|
|
102
|
+
resolvedBase = resolvedTargetDir;
|
|
103
|
+
} else {
|
|
104
|
+
resolvedBase = path.resolve(this.storagePath, targetDir);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
resolvedBase = resolvedRoot;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If fileName provided, normalize and resolve
|
|
111
|
+
let resolvedPath;
|
|
112
|
+
if (fileName) {
|
|
113
|
+
const normalizedFileName = this._normalizeStorageRelativePath(fileName);
|
|
114
|
+
resolvedPath = path.resolve(resolvedBase, normalizedFileName);
|
|
115
|
+
} else {
|
|
116
|
+
resolvedPath = resolvedBase;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate the resolved path is strictly inside the storage root (not equal to it)
|
|
120
|
+
const relative = path.relative(resolvedRoot, resolvedPath);
|
|
121
|
+
if (relative === '' || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
|
|
122
|
+
throw new errors.IncorrectUsageError({
|
|
123
|
+
message: tpl(messages.invalidPathParameter, {path: fileName || targetDir || ''})
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return resolvedPath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Saves the file to storage (the file system)
|
|
132
|
+
* - returns a promise which ultimately returns the full url to the uploaded file
|
|
133
|
+
*/
|
|
134
|
+
async save(file: StorageFile, targetDir?: string): Promise<string> {
|
|
135
|
+
let targetFilename;
|
|
136
|
+
|
|
137
|
+
targetDir = targetDir
|
|
138
|
+
? this._resolveAndValidateStoragePath(targetDir)
|
|
139
|
+
: this.getTargetDir(this.storagePath);
|
|
140
|
+
|
|
141
|
+
const filename = await this.getUniqueFileName(file, targetDir);
|
|
142
|
+
|
|
143
|
+
targetFilename = filename;
|
|
144
|
+
|
|
145
|
+
// Verify that we are saving directly under `targetDir` and not outside of it.
|
|
146
|
+
const expectedPrefix = path.join(path.resolve(targetDir), '/');
|
|
147
|
+
if (!path.resolve(targetFilename).startsWith(expectedPrefix)) {
|
|
148
|
+
throw new errors.BadRequestError({message: 'Cannot save to the given filename'});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await fs.mkdirs(targetDir);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await fs.copy(file.path, targetFilename);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if ((err as NodeJS.ErrnoException).code === 'ENAMETOOLONG') {
|
|
157
|
+
throw new errors.BadRequestError({err: errify(err)});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The src for the image must be in URI format, not a file system path, which in Windows uses \
|
|
164
|
+
// For local file system storage can use relative path so add a slash
|
|
165
|
+
const fullUrl = (
|
|
166
|
+
urlUtils.urlJoin('/',
|
|
167
|
+
urlUtils.getSubdir(),
|
|
168
|
+
this.staticFileURLPrefix as string,
|
|
169
|
+
path.relative(this.storagePath, targetFilename))
|
|
170
|
+
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
|
171
|
+
|
|
172
|
+
return fullUrl;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Saves a buffer in the targetPath
|
|
177
|
+
*
|
|
178
|
+
* @param buffer is an instance of Buffer
|
|
179
|
+
* @param targetPath relative path NOT including storage path to which the buffer should be written
|
|
180
|
+
* @returns a URL to retrieve the data
|
|
181
|
+
*/
|
|
182
|
+
async saveRaw(buffer: Buffer, targetPath: string): Promise<string> {
|
|
183
|
+
const storagePath = path.join(this.storagePath, this._normalizeStorageRelativePath(targetPath));
|
|
184
|
+
const targetDir = path.dirname(storagePath);
|
|
185
|
+
|
|
186
|
+
await fs.mkdirs(targetDir);
|
|
187
|
+
await fs.writeFile(storagePath, buffer);
|
|
188
|
+
|
|
189
|
+
// For local file system storage can use relative path so add a slash
|
|
190
|
+
const fullUrl = (
|
|
191
|
+
urlUtils.urlJoin('/', urlUtils.getSubdir(),
|
|
192
|
+
this.staticFileURLPrefix as string,
|
|
193
|
+
targetPath)
|
|
194
|
+
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
|
195
|
+
|
|
196
|
+
return fullUrl;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param url full url under which the stored content is served, result of save method
|
|
201
|
+
* @returns relative path under which the content is stored
|
|
202
|
+
*/
|
|
203
|
+
urlToPath(url: string): string {
|
|
204
|
+
let relativePath;
|
|
205
|
+
|
|
206
|
+
const prefix = urlUtils.urlJoin('/',
|
|
207
|
+
urlUtils.getSubdir(),
|
|
208
|
+
this.staticFileURLPrefix as string
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (url.startsWith(this.staticFileUrl)) {
|
|
212
|
+
// CASE: full path that includes the site url
|
|
213
|
+
relativePath = url.replace(this.staticFileUrl, '');
|
|
214
|
+
} else if (url.startsWith(prefix)) {
|
|
215
|
+
// CASE: The result of the save method doesn't include the site url. So we need to handle this case.
|
|
216
|
+
relativePath = url.replace(prefix, '');
|
|
217
|
+
} else {
|
|
218
|
+
throw new errors.IncorrectUsageError({
|
|
219
|
+
message: tpl(messages.invalidUrlParameter, {url})
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return this._normalizeStorageRelativePath(relativePath);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
throw new errors.IncorrectUsageError({
|
|
227
|
+
message: tpl(messages.invalidUrlParameter, {url})
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async exists(fileName: string, targetDir?: string): Promise<boolean> {
|
|
233
|
+
let filePath;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
filePath = this._resolveAndValidateStoragePath(targetDir, fileName);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err instanceof errors.IncorrectUsageError) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await fs.stat(filePath);
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* For some reason send divides the max age number by 1000
|
|
255
|
+
* Fallthrough: false ensures that if an image isn't found, it automatically 404s
|
|
256
|
+
* Wrap server static errors
|
|
257
|
+
*/
|
|
258
|
+
serve(): RequestHandler {
|
|
259
|
+
const {storagePath, errorMessages} = this;
|
|
260
|
+
|
|
261
|
+
return function serveStaticContent(req: Request, res: Response, next: NextFunction) {
|
|
262
|
+
return serveStatic(
|
|
263
|
+
storagePath,
|
|
264
|
+
{
|
|
265
|
+
maxAge: (365 * 24 * 60 * 60 * 1000), // 1 year in ms
|
|
266
|
+
fallthrough: false
|
|
267
|
+
}
|
|
268
|
+
)(req, res, (err?: ServeStaticError) => {
|
|
269
|
+
if (err) {
|
|
270
|
+
if (err.statusCode === 404) {
|
|
271
|
+
return next(new errors.NotFoundError({
|
|
272
|
+
message: tpl(errorMessages.notFound),
|
|
273
|
+
code: 'STATIC_FILE_NOT_FOUND',
|
|
274
|
+
property: err.path
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (err.statusCode === 400) {
|
|
279
|
+
return next(new errors.BadRequestError({err: err}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (err.statusCode === 403) {
|
|
283
|
+
return next(new errors.NoPermissionError({err: err}));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (err.name === 'RangeNotSatisfiableError') {
|
|
287
|
+
return next(new errors.RangeNotSatisfiableError({err}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return next(new errors.InternalServerError({err: err}));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
next();
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async delete(fileName: string, targetDir?: string): Promise<void> {
|
|
299
|
+
const filePath = this._resolveAndValidateStoragePath(targetDir, fileName);
|
|
300
|
+
return await fs.remove(filePath);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Reads bytes from disk for a target file
|
|
305
|
+
* - path of target file (without content path!)
|
|
306
|
+
*/
|
|
307
|
+
async read(options?: Partial<ReadOptions>): Promise<Buffer> {
|
|
308
|
+
options = options || {};
|
|
309
|
+
|
|
310
|
+
const normalizedPath = this._normalizeStorageRelativePath(options.path);
|
|
311
|
+
const targetPath = path.join(this.storagePath, normalizedPath);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
return await fs.readFile(targetPath);
|
|
315
|
+
} catch (rawError) {
|
|
316
|
+
const err = errify(rawError);
|
|
317
|
+
const code = (rawError as NodeJS.ErrnoException).code;
|
|
318
|
+
|
|
319
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
320
|
+
throw new errors.NotFoundError({
|
|
321
|
+
err: err,
|
|
322
|
+
message: tpl(this.errorMessages.notFoundWithRef, {file: options.path})
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (code === 'ENAMETOOLONG') {
|
|
327
|
+
throw new errors.BadRequestError({err: err});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (code === 'EACCES') {
|
|
331
|
+
throw new errors.NoPermissionError({err: err});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
throw new errors.InternalServerError({
|
|
335
|
+
err: err,
|
|
336
|
+
message: tpl(this.errorMessages.cannotRead, {file: options.path})
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default LocalStorageBase;
|
|
@@ -1,68 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getLocalImagesStoragePath = getLocalImagesStoragePath;
|
|
7
|
+
exports.isLocalImage = isLocalImage;
|
|
8
|
+
exports.isInternalImage = isInternalImage;
|
|
9
|
+
const config_1 = __importDefault(require("../../../shared/config"));
|
|
10
|
+
const url_utils_1 = __importDefault(require("../../../shared/url-utils"));
|
|
3
11
|
/**
|
|
4
12
|
* @TODO: move `events.js` to here - e.g. storageUtils.getStorage
|
|
5
13
|
*/
|
|
6
|
-
|
|
7
14
|
/**
|
|
8
|
-
* Sanitizes a given URL or path for an image to be readable by the local file
|
|
9
|
-
* as storage needs the path without `/content/images/` prefix
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @description Takes a url or filepath and returns a filepath with is readable
|
|
14
|
-
* for the local file storage.
|
|
15
|
+
* Sanitizes a given URL or path for an image to be readable by the local file
|
|
16
|
+
* storage, as storage needs the path without the `/content/images/` prefix.
|
|
17
|
+
*
|
|
18
|
+
* Takes a url or filepath and returns a filepath which is readable for the
|
|
19
|
+
* local file storage.
|
|
15
20
|
*/
|
|
16
|
-
|
|
21
|
+
function getLocalImagesStoragePath(imagePath) {
|
|
17
22
|
// The '/' in urlJoin is necessary to add the '/' to `content/images`, if no subdirectory is setup
|
|
18
|
-
const urlRegExp = new RegExp(`^${
|
|
19
|
-
|
|
20
|
-
urlUtils.getSubdir(),
|
|
21
|
-
'/',
|
|
22
|
-
urlUtils.STATIC_IMAGE_URL_PREFIX)}`
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const filePathRegExp = new RegExp(`^${urlUtils.urlJoin(
|
|
26
|
-
urlUtils.getSubdir(),
|
|
27
|
-
'/',
|
|
28
|
-
urlUtils.STATIC_IMAGE_URL_PREFIX)}`
|
|
29
|
-
);
|
|
30
|
-
|
|
23
|
+
const urlRegExp = new RegExp(`^${url_utils_1.default.urlJoin(url_utils_1.default.urlFor('home', true), url_utils_1.default.getSubdir(), '/', url_utils_1.default.STATIC_IMAGE_URL_PREFIX)}`);
|
|
24
|
+
const filePathRegExp = new RegExp(`^${url_utils_1.default.urlJoin(url_utils_1.default.getSubdir(), '/', url_utils_1.default.STATIC_IMAGE_URL_PREFIX)}`);
|
|
31
25
|
if (imagePath.match(urlRegExp)) {
|
|
32
26
|
return imagePath.replace(urlRegExp, '');
|
|
33
|
-
}
|
|
27
|
+
}
|
|
28
|
+
else if (imagePath.match(filePathRegExp)) {
|
|
34
29
|
return imagePath.replace(filePathRegExp, '');
|
|
35
|
-
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
36
32
|
return imagePath;
|
|
37
33
|
}
|
|
38
|
-
}
|
|
39
|
-
|
|
34
|
+
}
|
|
40
35
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* @
|
|
36
|
+
* Compares the imagePath with a regex that reflects our local file storage
|
|
37
|
+
*
|
|
38
|
+
* @param imagePath as URL or filepath
|
|
44
39
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (localImagePath !== imagePath) {
|
|
50
|
-
return true;
|
|
51
|
-
} else {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
40
|
+
function isLocalImage(imagePath) {
|
|
41
|
+
return getLocalImagesStoragePath(imagePath) !== imagePath;
|
|
42
|
+
}
|
|
56
43
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* @
|
|
44
|
+
* Checks whether the image is managed by Ghost storage (local or CDN)
|
|
45
|
+
*
|
|
46
|
+
* @param imagePath as URL or filepath
|
|
60
47
|
*/
|
|
61
|
-
|
|
62
|
-
if (
|
|
48
|
+
function isInternalImage(imagePath) {
|
|
49
|
+
if (isLocalImage(imagePath)) {
|
|
63
50
|
return true;
|
|
64
51
|
}
|
|
65
|
-
|
|
66
|
-
const imageBaseUrl = (config.get('urls:image') || '').replace(/\/+$/, '');
|
|
52
|
+
const imageBaseUrl = (config_1.default.get('urls:image') || '').replace(/\/+$/, '');
|
|
67
53
|
return !!(imageBaseUrl && imagePath.startsWith(imageBaseUrl + '/'));
|
|
68
|
-
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import config from '../../../shared/config';
|
|
2
|
+
import urlUtils from '../../../shared/url-utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @TODO: move `events.js` to here - e.g. storageUtils.getStorage
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanitizes a given URL or path for an image to be readable by the local file
|
|
10
|
+
* storage, as storage needs the path without the `/content/images/` prefix.
|
|
11
|
+
*
|
|
12
|
+
* Takes a url or filepath and returns a filepath which is readable for the
|
|
13
|
+
* local file storage.
|
|
14
|
+
*/
|
|
15
|
+
export function getLocalImagesStoragePath(imagePath: string): string {
|
|
16
|
+
// The '/' in urlJoin is necessary to add the '/' to `content/images`, if no subdirectory is setup
|
|
17
|
+
const urlRegExp = new RegExp(`^${urlUtils.urlJoin(
|
|
18
|
+
urlUtils.urlFor('home', true),
|
|
19
|
+
urlUtils.getSubdir(),
|
|
20
|
+
'/',
|
|
21
|
+
urlUtils.STATIC_IMAGE_URL_PREFIX)}`
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const filePathRegExp = new RegExp(`^${urlUtils.urlJoin(
|
|
25
|
+
urlUtils.getSubdir(),
|
|
26
|
+
'/',
|
|
27
|
+
urlUtils.STATIC_IMAGE_URL_PREFIX)}`
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (imagePath.match(urlRegExp)) {
|
|
31
|
+
return imagePath.replace(urlRegExp, '');
|
|
32
|
+
} else if (imagePath.match(filePathRegExp)) {
|
|
33
|
+
return imagePath.replace(filePathRegExp, '');
|
|
34
|
+
} else {
|
|
35
|
+
return imagePath;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compares the imagePath with a regex that reflects our local file storage
|
|
41
|
+
*
|
|
42
|
+
* @param imagePath as URL or filepath
|
|
43
|
+
*/
|
|
44
|
+
export function isLocalImage(imagePath: string): boolean {
|
|
45
|
+
return getLocalImagesStoragePath(imagePath) !== imagePath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Checks whether the image is managed by Ghost storage (local or CDN)
|
|
50
|
+
*
|
|
51
|
+
* @param imagePath as URL or filepath
|
|
52
|
+
*/
|
|
53
|
+
export function isInternalImage(imagePath: string): boolean {
|
|
54
|
+
if (isLocalImage(imagePath)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const imageBaseUrl = (config.get('urls:image') || '').replace(/\/+$/, '');
|
|
59
|
+
return !!(imageBaseUrl && imagePath.startsWith(imageBaseUrl + '/'));
|
|
60
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const dbBackup = require('../../data/db/backup');
|
|
3
4
|
const exporter = require('../../data/exporter');
|
|
4
5
|
const importer = require('../../data/importer');
|
|
@@ -29,6 +30,11 @@ const controller = {
|
|
|
29
30
|
}
|
|
30
31
|
},
|
|
31
32
|
query(frame) {
|
|
33
|
+
const filename = frame.options.filename;
|
|
34
|
+
if (filename && path.basename(filename) !== filename) {
|
|
35
|
+
throw new errors.ValidationError({message: 'Export filename must not contain path separators'});
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
// NOTE: we need to have `include` property available as backupDatabase uses it internally
|
|
33
39
|
Object.assign(frame.options, {include: frame.options.withRelated});
|
|
34
40
|
|
|
@@ -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
|
}
|
|
@@ -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)
|