## Fix - Use `[Config].upload.skipSafeFetch` to allow specific external urls - Use `[Config].upload.pasteURL.allowList` to allow specific external urls Documentation: [Uploading files from remote urls](https://payloadcms.com/docs/upload/overview#uploading-files-from-remote-urls) Fixes: https://github.com/payloadcms/payload/issues/12876 Mentioned: https://github.com/payloadcms/payload/issues/7037, https://github.com/payloadcms/payload/issues/12934 Source PR: https://github.com/payloadcms/payload/pull/12622 Issue Trace: 1. [`allowList` Added](8b7f2ddbf4 (diff-92acf7b8d30e447a791e37820136bcbf23c42f0358daca0fdea4e7b77f7d4bc9)) 2. [`allowList` Removed](648c168f86 (diff-92acf7b8d30e447a791e37820136bcbf23c42f0358daca0fdea4e7b77f7d4bc9))
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import type { Config } from 'payload'
|
|
|
|
import type { AllowList, PluginOptions } from './types.js'
|
|
|
|
import { getFields } from './fields/getFields.js'
|
|
import { getAfterDeleteHook } from './hooks/afterDelete.js'
|
|
import { getBeforeChangeHook } from './hooks/beforeChange.js'
|
|
|
|
// This plugin extends all targeted collections by offloading uploaded files
|
|
// to cloud storage instead of solely storing files locally.
|
|
|
|
// It is based on an adapter approach, where adapters can be written for any cloud provider.
|
|
// Adapters are responsible for providing four actions that this plugin will use:
|
|
// 1. handleUpload, 2. handleDelete, 3. generateURL, 4. staticHandler
|
|
|
|
// Optionally, the adapter can specify any Webpack config overrides if they are necessary.
|
|
|
|
export const cloudStoragePlugin =
|
|
(pluginOptions: PluginOptions) =>
|
|
(incomingConfig: Config): Config => {
|
|
const { collections: allCollectionOptions, enabled } = pluginOptions
|
|
const config = { ...incomingConfig }
|
|
|
|
// Return early if disabled. Only webpack config mods are applied.
|
|
if (enabled === false) {
|
|
return config
|
|
}
|
|
|
|
const initFunctions: Array<() => void> = []
|
|
|
|
return {
|
|
...config,
|
|
collections: (config.collections || []).map((existingCollection) => {
|
|
const options = allCollectionOptions[existingCollection.slug]
|
|
|
|
if (options?.adapter) {
|
|
const adapter = options.adapter({
|
|
collection: existingCollection,
|
|
prefix: options.prefix,
|
|
})
|
|
|
|
if (adapter.onInit) {
|
|
initFunctions.push(adapter.onInit)
|
|
}
|
|
|
|
const fields = getFields({
|
|
adapter,
|
|
collection: existingCollection,
|
|
disablePayloadAccessControl: options.disablePayloadAccessControl,
|
|
generateFileURL: options.generateFileURL,
|
|
prefix: options.prefix,
|
|
})
|
|
|
|
const handlers = [
|
|
...(typeof existingCollection.upload === 'object' &&
|
|
Array.isArray(existingCollection.upload.handlers)
|
|
? existingCollection.upload.handlers
|
|
: []),
|
|
]
|
|
|
|
if (!options.disablePayloadAccessControl) {
|
|
handlers.push(adapter.staticHandler)
|
|
// Else if disablePayloadAccessControl: true and clientUploads is used
|
|
// Build the "proxied" handler that responses only when the file was requested by client upload in addDataAndFileToRequest
|
|
} else if (adapter.clientUploads) {
|
|
handlers.push((req, args) => {
|
|
if ('clientUploadContext' in args.params) {
|
|
return adapter.staticHandler(req, args)
|
|
}
|
|
})
|
|
}
|
|
|
|
const getSkipSafeFetchSetting = (): AllowList | boolean => {
|
|
if (options.disablePayloadAccessControl) {
|
|
return true
|
|
}
|
|
const isBooleanTrueSkipSafeFetch =
|
|
typeof existingCollection.upload === 'object' &&
|
|
existingCollection.upload.skipSafeFetch === true
|
|
|
|
const isAllowListSkipSafeFetch =
|
|
typeof existingCollection.upload === 'object' &&
|
|
Array.isArray(existingCollection.upload.skipSafeFetch)
|
|
|
|
if (isBooleanTrueSkipSafeFetch) {
|
|
return true
|
|
} else if (isAllowListSkipSafeFetch) {
|
|
const existingSkipSafeFetch =
|
|
typeof existingCollection.upload === 'object' &&
|
|
Array.isArray(existingCollection.upload.skipSafeFetch)
|
|
? existingCollection.upload.skipSafeFetch
|
|
: []
|
|
|
|
const hasExactLocalhostMatch = existingSkipSafeFetch.some((entry) => {
|
|
const entryKeys = Object.keys(entry)
|
|
return entryKeys.length === 1 && entry.hostname === 'localhost'
|
|
})
|
|
|
|
const localhostEntry =
|
|
process.env.NODE_ENV !== 'production' && !hasExactLocalhostMatch
|
|
? [{ hostname: 'localhost' }]
|
|
: []
|
|
|
|
return [...existingSkipSafeFetch, ...localhostEntry]
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
return [{ hostname: 'localhost' }]
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
return {
|
|
...existingCollection,
|
|
fields,
|
|
hooks: {
|
|
...(existingCollection.hooks || {}),
|
|
afterDelete: [
|
|
...(existingCollection.hooks?.afterDelete || []),
|
|
getAfterDeleteHook({ adapter, collection: existingCollection }),
|
|
],
|
|
beforeChange: [
|
|
...(existingCollection.hooks?.beforeChange || []),
|
|
getBeforeChangeHook({ adapter, collection: existingCollection }),
|
|
],
|
|
},
|
|
upload: {
|
|
...(typeof existingCollection.upload === 'object' ? existingCollection.upload : {}),
|
|
adapter: adapter.name,
|
|
disableLocalStorage:
|
|
typeof options.disableLocalStorage === 'boolean'
|
|
? options.disableLocalStorage
|
|
: true,
|
|
handlers,
|
|
skipSafeFetch: getSkipSafeFetchSetting(),
|
|
},
|
|
}
|
|
}
|
|
|
|
return existingCollection
|
|
}),
|
|
onInit: async (payload) => {
|
|
initFunctions.forEach((fn) => fn())
|
|
if (config.onInit) {
|
|
await config.onInit(payload)
|
|
}
|
|
},
|
|
}
|
|
}
|