From 351251d313e8c283745a2f72779e0bf28506e45e Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Wed, 8 Jan 2025 07:26:18 +0100 Subject: [PATCH] Allow to specify which files choosen from CKBox are downloadable. --- packages/ckeditor5-ckbox/src/ckboxcommand.ts | 44 +++++- packages/ckeditor5-ckbox/src/ckboxconfig.ts | 18 +++ .../ckeditor5-ckbox/tests/ckboxcommand.js | 138 ++++++++++++++++++ .../ckeditor5-ckbox/tests/manual/ckbox.js | 3 +- 4 files changed, 194 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-ckbox/src/ckboxcommand.ts b/packages/ckeditor5-ckbox/src/ckboxcommand.ts index d63613c0711..9b96e786b5f 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( downloadableFilesConfig, asset ) } 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 config The CKBox download asset configuration. + * @param asset The asset to prepare the attributes for. */ -function prepareLinkAssetAttributes( asset: CKBoxRawAssetDefinition ): CKBoxAssetLinkAttributesDefinition { +function prepareLinkAssetAttributes( + config: CKBoxConfig['downloadableFiles'], + asset: CKBoxRawAssetDefinition +): CKBoxAssetLinkAttributesDefinition { return { linkName: asset.data.name, - linkHref: getAssetUrl( asset ) + linkHref: getAssetUrl( config, asset ) }; } @@ -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 config The CKBox download asset configuration. + * @param asset The asset to create the URL for. */ -function getAssetUrl( asset: CKBoxRawAssetDefinition ) { +function getAssetUrl( config: CKBoxConfig['downloadableFiles'], asset: CKBoxRawAssetDefinition ) { const url = new URL( asset.data.url ); - url.searchParams.set( 'download', 'true' ); + if ( isDownloadableAsset( config, asset ) ) { + url.searchParams.set( 'download', 'true' ); + } return url.toString(); } +/** + * Determines if download should be enabled for given asset based on configuration. + * + * @param config The CKBox download asset configuration. + * @param asset The asset to check. + */ +function isDownloadableAsset( + config: CKBoxConfig['downloadableFiles'], + asset: CKBoxRawAssetDefinition +): 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 f53cd9c363a..f1d65b05848 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 caa378a16c0..ab1639d7832 100644 --- a/packages/ckeditor5-ckbox/tests/ckboxcommand.js +++ b/packages/ckeditor5-ckbox/tests/ckboxcommand.js @@ -1209,6 +1209,144 @@ 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', + downloadableFiles: false + } + } ); + + 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 1f6ecb67d73..c446adc320c 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: [ /^(?!pdf$).*$/ ] } } ) .then( editor => {