From bf21d480c18316f419eb19a3ab3c07615264557f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Bagi=C5=84ski?= Date: Wed, 22 Jan 2025 11:39:11 +0100 Subject: [PATCH] Allow to specify which files choosen from CKBox are downloadable. (#17712) Feature (ckbox): Allow to specify which files chosen from CKBox are downloadable. Closes #15928 --- packages/ckeditor5-ckbox/src/ckboxcommand.ts | 44 +++++- packages/ckeditor5-ckbox/src/ckboxconfig.ts | 18 +++ .../ckeditor5-ckbox/tests/ckboxcommand.js | 137 ++++++++++++++++++ .../ckeditor5-ckbox/tests/manual/ckbox.js | 3 +- 4 files changed, 193 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxcommand.ts b/packages/ckeditor5-ckbox/src/ckboxcommand.ts index c71d4a7c072..670089d2797 100644 --- a/packages/ckeditor5-ckbox/src/ckboxcommand.ts +++ b/packages/ckeditor5-ckbox/src/ckboxcommand.ts @@ -19,6 +19,7 @@ import type { CKBoxAssetImageDefinition, CKBoxAssetLinkAttributesDefinition, CKBoxAssetLinkDefinition, + CKBoxConfig, CKBoxRawAssetDefinition } from './ckboxconfig.js'; @@ -189,6 +190,7 @@ export default class CKBoxCommand extends Command { const editor = this.editor; const model = editor.model; const shouldInsertDataId = !editor.config.get( 'ckbox.ignoreDataId' ); + const downloadableFilesConfig = editor.config.get( 'ckbox.downloadableFiles' ); // Refresh the command after firing the `ckbox:*` event. this.on( 'ckbox', () => { @@ -230,6 +232,7 @@ export default class CKBoxCommand extends Command { const assetsToProcess = prepareAssets( { assets, + downloadableFilesConfig, isImageAllowed: imageCommand.isEnabled, isLinkAllowed: linkCommand.isEnabled } ); @@ -379,7 +382,8 @@ export default class CKBoxCommand extends Command { * Parses the chosen assets into the internal data format. Filters out chosen assets that are not allowed. */ function prepareAssets( - { assets, isImageAllowed, isLinkAllowed }: { + { downloadableFilesConfig, assets, isImageAllowed, isLinkAllowed }: { + downloadableFilesConfig: CKBoxConfig[ 'downloadableFiles' ]; assets: Array; isImageAllowed: boolean; isLinkAllowed: boolean; @@ -395,7 +399,7 @@ function prepareAssets( { id: asset.data.id, type: 'link', - attributes: prepareLinkAssetAttributes( asset ) + attributes: prepareLinkAssetAttributes( asset, downloadableFilesConfig ) } as const ) .filter( asset => asset.type === 'image' ? isImageAllowed : isLinkAllowed ); @@ -424,12 +428,16 @@ export function prepareImageAssetAttributes( asset: CKBoxRawAssetDefinition ): C /** * Parses the assets attributes into the internal data format. * - * @param origin The base URL for assets inserted into the editor. + * @param asset The asset to prepare the attributes for. + * @param config The CKBox download asset configuration. */ -function prepareLinkAssetAttributes( asset: CKBoxRawAssetDefinition ): CKBoxAssetLinkAttributesDefinition { +function prepareLinkAssetAttributes( + asset: CKBoxRawAssetDefinition, + config: CKBoxConfig[ 'downloadableFiles' ] +): CKBoxAssetLinkAttributesDefinition { return { linkName: asset.data.name, - linkHref: getAssetUrl( asset ) + linkHref: getAssetUrl( asset, config ) }; } @@ -449,16 +457,36 @@ function isImage( asset: CKBoxRawAssetDefinition ) { /** * Creates the URL for the asset. * - * @param origin The base URL for assets inserted into the editor. + * @param asset The asset to create the URL for. + * @param config The CKBox download asset configuration. */ -function getAssetUrl( asset: CKBoxRawAssetDefinition ) { +function getAssetUrl( asset: CKBoxRawAssetDefinition, config: CKBoxConfig[ 'downloadableFiles' ] ) { const url = new URL( asset.data.url ); - url.searchParams.set( 'download', 'true' ); + if ( isDownloadableAsset( asset, config ) ) { + url.searchParams.set( 'download', 'true' ); + } return url.toString(); } +/** + * Determines if download should be enabled for given asset based on configuration. + * + * @param asset The asset to check. + * @param config The CKBox download asset configuration. + */ +function isDownloadableAsset( + asset: CKBoxRawAssetDefinition, + config: CKBoxConfig[ 'downloadableFiles' ] +): boolean { + if ( typeof config === 'function' ) { + return config( asset ); + } + + return true; +} + /** * Fired when the command is executed, the dialog is closed or the assets are chosen. * diff --git a/packages/ckeditor5-ckbox/src/ckboxconfig.ts b/packages/ckeditor5-ckbox/src/ckboxconfig.ts index f189447e283..0fe9adefd59 100644 --- a/packages/ckeditor5-ckbox/src/ckboxconfig.ts +++ b/packages/ckeditor5-ckbox/src/ckboxconfig.ts @@ -173,6 +173,19 @@ export interface CKBoxConfig { * ``` */ choosableFileExtensions?: Array; + + /** + * Controls when to enable the download attribute for inserted links. + * + * By default, files are downloadable. + * + * ```ts + * const ckboxConfig = { + * downloadableFiles: asset => asset.data.extension !== 'pdf' + * }; + * ``` + */ + downloadableFiles?: ( asset: CKBoxRawAssetDefinition ) => boolean; } export interface CKBoxDialogConfig { @@ -461,6 +474,11 @@ export interface CKBoxRawAssetDataDefinition { * The asset location. */ url: string; + + /** + * The asset type. + */ + extension?: string; } /** diff --git a/packages/ckeditor5-ckbox/tests/ckboxcommand.js b/packages/ckeditor5-ckbox/tests/ckboxcommand.js index c4314863902..7eb2a447be1 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxcommand.js +++ b/packages/ckeditor5-ckbox/tests/ckboxcommand.js @@ -1209,6 +1209,143 @@ describe( 'CKBoxCommand', () => { sinon.assert.calledOnce( focusSpy ); } ); + + describe( 'downloadable files configuration', () => { + let command; + + beforeEach( async () => { + assets = { + images: [ + { + data: { + id: 'image-id1', + extension: 'png', + metadata: { + width: 100, + height: 100 + }, + name: 'image1', + imageUrls: { + 100: 'https://example.com/workspace1/assets/image-id1/images/100.webp', + default: 'https://example.com/workspace1/assets/image-id1/images/100.png' + }, + url: 'https://example.com/workspace1/assets/image-id1/file' + } + } + ], + links: [ + { + data: { + id: 'link-id1', + extension: 'pdf', + name: 'file1', + url: 'https://example.com/workspace1/assets/link-id1/file' + } + }, + { + data: { + id: 'link-id2', + extension: 'zip', + name: 'file2', + url: 'https://example.com/workspace1/assets/link-id2/file' + } + } + ] + }; + } ); + + it( 'should add download parameter to URLs by default', async () => { + const editor = await createTestEditor( { + ckbox: { + tokenUrl: 'foo' + } + } ); + + command = editor.commands.get( 'ckbox' ); + onChoose = command._prepareOptions().assets.onChoose; + + onChoose( [ assets.links[ 1 ] ] ); + + expect( getModelData( editor.model ) ).to.equal( + '' + + '[<$text ' + + 'ckboxLinkId="link-id2" ' + + 'linkHref="https://example.com/workspace1/assets/link-id2/file?download=true">' + + 'file2' + + ']' + + '' + ); + + await editor.destroy(); + } ); + + it( 'should allow custom function for determining downloadable files', async () => { + const editor = await createTestEditor( { + ckbox: { + tokenUrl: 'foo', + downloadableFiles: asset => asset.data.name === 'file1' + } + } ); + + const command = editor.commands.get( 'ckbox' ); + const onChoose = command._prepareOptions().assets.onChoose; + + // `file1` should have download parameter. + onChoose( [ assets.links[ 0 ] ] ); + + expect( getModelData( editor.model ) ).to.equal( + '' + + '[<$text ' + + 'ckboxLinkId="link-id1" ' + + 'linkHref="https://example.com/workspace1/assets/link-id1/file?download=true">' + + 'file1' + + ']' + + '' + ); + + // `file2` should not have download parameter. + editor.setData( '' ); + onChoose( [ assets.links[ 1 ] ] ); + + expect( getModelData( editor.model ) ).to.equal( + '' + + '[<$text ' + + 'ckboxLinkId="link-id2" ' + + 'linkHref="https://example.com/workspace1/assets/link-id2/file">' + + 'file2' + + ']' + + '' + ); + + await editor.destroy(); + } ); + + it( 'should not affect image assets', async () => { + const editor = await createTestEditor( { + ckbox: { + tokenUrl: 'foo' + } + } ); + + const command = editor.commands.get( 'ckbox' ); + const onChoose = command._prepareOptions().assets.onChoose; + + onChoose( [ assets.images[ 0 ] ] ); + + expect( getModelData( editor.model ) ).to.equal( + '[' + + ']' + ); + + await editor.destroy(); + } ); + } ); } ); } ); } ); diff --git a/packages/ckeditor5-ckbox/tests/manual/ckbox.js b/packages/ckeditor5-ckbox/tests/manual/ckbox.js index 9e2d0cc875c..601f194ec71 100644 --- a/packages/ckeditor5-ckbox/tests/manual/ckbox.js +++ b/packages/ckeditor5-ckbox/tests/manual/ckbox.js @@ -55,7 +55,8 @@ ClassicEditor ckbox: { tokenUrl: TOKEN_URL, forceDemoLabel: true, - allowExternalImagesEditing: [ /^data:/, /^i.imgur.com\//, 'origin' ] + allowExternalImagesEditing: [ /^data:/, /^i.imgur.com\//, 'origin' ], + downloadableFiles: asset => asset.data.extension !== 'pdf' } } ) .then( editor => {