From 745f3966d7a8a3c766b40fcffbbcc98c4844ca16 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 20 Feb 2026 14:20:35 +0100 Subject: [PATCH 01/15] fix: generate correct curl download URLs using backend handle endpoint Updates the curl command generation to use the new backend endpoint GET /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} instead of the non-existent /api/bitstream/{handle}/{seq}/{filename}. Key changes: - Uses correct backend endpoint path: /core/bitstreams/handle/{handle}/ - Removes unnecessary sequence index from URLs (uses filename only) - Quotes the URL to prevent shell brace expansion - For single file, uses -o with explicit filename Fixes: #1210 --- .../clarin-files-section.component.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 08edfe7ac55..6758b017ae8 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -107,10 +107,15 @@ export class ClarinFilesSectionComponent implements OnInit { return file.name; }); - // Generate curl command for individual bitstream downloads - const baseUrl = `${this.halService.getRootHref()}/bitstream/${this.itemHandle}`; - const fileNamesFormatted = fileNames.map((fileName, index) => `/${index}/${fileName}`).join(','); - this.command = `curl -O ${baseUrl}{${fileNamesFormatted}}`; + // Generate curl command for individual bitstream downloads by handle + filename. + // Uses the backend endpoint: /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} + const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; + if (fileNames.length === 1) { + this.command = `curl -o '${fileNames[0]}' '${baseUrl}/${fileNames[0]}'`; + } else { + const fileNamesFormatted = fileNames.map(fileName => `/${fileName}`).join(','); + this.command = `curl -O '${baseUrl}{${fileNamesFormatted}}'`; + } } loadDownloadZipConfigProperties() { From 7b9a8f660060616b5a54c5cd69912cce8877075e Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 20 Feb 2026 15:53:18 +0100 Subject: [PATCH 02/15] Fixed formatting of the file names --- .../clarin-files-section.component.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 6758b017ae8..af7f837ce2a 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -111,13 +111,22 @@ export class ClarinFilesSectionComponent implements OnInit { // Uses the backend endpoint: /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; if (fileNames.length === 1) { - this.command = `curl -o '${fileNames[0]}' '${baseUrl}/${fileNames[0]}'`; + const encodedName = this.encodeFilenameForUrl(fileNames[0]); + this.command = `curl -o "${fileNames[0]}" "${baseUrl}/${encodedName}"`; } else { - const fileNamesFormatted = fileNames.map(fileName => `/${fileName}`).join(','); - this.command = `curl -O '${baseUrl}{${fileNamesFormatted}}'`; + const fileNamesFormatted = fileNames.map(fileName => `/${this.encodeFilenameForUrl(fileName)}`).join(','); + this.command = `curl -O "${baseUrl}{${fileNamesFormatted}}"`; } } + /** + * Encode a filename for use in a URL path segment. + * Encodes special characters (spaces, parentheses, +, etc.) using percent-encoding. + */ + private encodeFilenameForUrl(filename: string): string { + return encodeURIComponent(filename).replace(/[()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); + } + loadDownloadZipConfigProperties() { this.configurationService.findByPropertyName('download.all.limit.min.file.count') .pipe( From 07b0ee1c711bf8703b0d9d4e257d35718a843a2c Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 20 Feb 2026 15:55:09 +0100 Subject: [PATCH 03/15] fix: use -o with real filename to avoid percent-encoded names from curl -O curl -O uses the URL path as the saved filename, so percent-encoded characters (e.g. %20, %2B, %28) stay encoded in the output file. Now generates separate 'curl -o realname url' for each file joined with &&, ensuring files are saved with their actual names. --- .../clarin-files-section.component.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index af7f837ce2a..54961724dc3 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -109,14 +109,13 @@ export class ClarinFilesSectionComponent implements OnInit { // Generate curl command for individual bitstream downloads by handle + filename. // Uses the backend endpoint: /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} + // Always uses -o with the real filename to avoid percent-encoded filenames from -O. const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; - if (fileNames.length === 1) { - const encodedName = this.encodeFilenameForUrl(fileNames[0]); - this.command = `curl -o "${fileNames[0]}" "${baseUrl}/${encodedName}"`; - } else { - const fileNamesFormatted = fileNames.map(fileName => `/${this.encodeFilenameForUrl(fileName)}`).join(','); - this.command = `curl -O "${baseUrl}{${fileNamesFormatted}}"`; - } + const commands = fileNames.map(fileName => { + const encodedName = this.encodeFilenameForUrl(fileName); + return `curl -o "${fileName}" "${baseUrl}/${encodedName}"`; + }); + this.command = commands.join(' && '); } /** From eba6d174854363243a6430f19e948da8f25b6e7c Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 20 Feb 2026 15:58:10 +0100 Subject: [PATCH 04/15] fix: use curl -OJ with brace expansion for compact download command --- .../clarin-files-section.component.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 54961724dc3..22b35db8b27 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -109,13 +109,12 @@ export class ClarinFilesSectionComponent implements OnInit { // Generate curl command for individual bitstream downloads by handle + filename. // Uses the backend endpoint: /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} - // Always uses -o with the real filename to avoid percent-encoded filenames from -O. + // -J tells curl to use the filename from the Content-Disposition header (the real name) + // instead of the percent-encoded URL path. const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; - const commands = fileNames.map(fileName => { - const encodedName = this.encodeFilenameForUrl(fileName); - return `curl -o "${fileName}" "${baseUrl}/${encodedName}"`; - }); - this.command = commands.join(' && '); + const fileNamesFormatted = fileNames.map(fileName => `/${this.encodeFilenameForUrl(fileName)}`).join(','); + this.command = `curl -OJ "${baseUrl}{${fileNamesFormatted}}"`; + } /** From ee0cc10449a40cfac80750a45cbad22567c9ccd6 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 20 Feb 2026 16:29:54 +0100 Subject: [PATCH 05/15] Fixed linting error --- .../clarin-files-section/clarin-files-section.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 22b35db8b27..12e89b35c39 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -114,7 +114,6 @@ export class ClarinFilesSectionComponent implements OnInit { const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; const fileNamesFormatted = fileNames.map(fileName => `/${this.encodeFilenameForUrl(fileName)}`).join(','); this.command = `curl -OJ "${baseUrl}{${fileNamesFormatted}}"`; - } /** From da8baf75e8172b13d6ddd122261459e90ae82d73 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Tue, 24 Feb 2026 09:47:34 +0100 Subject: [PATCH 06/15] removed duplicates logic --- .../clarin-files-section/clarin-files-section.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 12e89b35c39..9324c430df4 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -1,4 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; +import { encodeRFC3986URIComponent } from '../../shared/clarin-shared-util'; import { Item } from '../../core/shared/item.model'; import { getAllSucceededRemoteListPayload, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getItemPageRoute } from '../item-page-routing-paths'; @@ -117,11 +118,10 @@ export class ClarinFilesSectionComponent implements OnInit { } /** - * Encode a filename for use in a URL path segment. - * Encodes special characters (spaces, parentheses, +, etc.) using percent-encoding. + * Encode a filename for use in a URL path segment using the shared RFC3986 utility. */ private encodeFilenameForUrl(filename: string): string { - return encodeURIComponent(filename).replace(/[()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); + return encodeRFC3986URIComponent(filename); } loadDownloadZipConfigProperties() { From 83e1b85c9849f9c2918777c5d1fd9d2daa796fda Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 24 Feb 2026 15:33:04 +0100 Subject: [PATCH 07/15] fix: use curl -o instead of -OJ to fix non-ASCII filenames on Windows curl -J (Content-Disposition) cannot create files with non-ASCII characters on Windows because it interprets the header bytes using the console code page. Changed to curl -o filename url format where the shell passes the filename directly to the OS, correctly handling Unicode on all platforms. Also added tests for UTF-8 filenames and double-quote escaping. --- .../clarin-files-section.component.spec.ts | 146 +++++++++++++++--- .../clarin-files-section.component.ts | 24 ++- 2 files changed, 138 insertions(+), 32 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts index a1455425ebf..83366391ea8 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts @@ -20,25 +20,32 @@ describe('ClarinFilesSectionComponent', () => { let fixture: ComponentFixture; let mockRegistryService: any; - let halService: HALEndpointService; + let halService: any; + + const ROOT_HREF = 'http://localhost:8080/server/api'; + + function createMetadataBitstream(name: string, canPreview: boolean = true): MetadataBitstream { + const bs = new MetadataBitstream(); + bs.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe'; + bs.name = name; + bs.description = 'test'; + bs.fileSize = 1024; + bs.checksum = 'abc'; + bs.type = new ResourceType('item'); + bs.fileInfo = []; + bs.format = 'text'; + bs.canPreview = canPreview; + bs._links = { + self: new HALLink(), + schema: new HALLink(), + }; + bs._links.self.href = ''; + bs._links.schema.href = ''; + return bs; + } + // Set up the mock service's getMetadataBitstream method to return a simple stream - const metadatabitstream = new MetadataBitstream(); - metadatabitstream.id = '70ccc608-f6a5-4c96-ab2d-53bc56ae8ebe'; - metadatabitstream.name = 'test'; - metadatabitstream.description = 'test'; - metadatabitstream.fileSize = 1024; - metadatabitstream.checksum = 'abc'; - metadatabitstream.type = new ResourceType('item'); - metadatabitstream.fileInfo = []; - metadatabitstream.format = 'text'; - metadatabitstream.canPreview = false; - metadatabitstream._links = { - self: new HALLink(), - schema: new HALLink(), - }; - - metadatabitstream._links.self.href = ''; - metadatabitstream._links.schema.href = ''; + const metadatabitstream = createMetadataBitstream('test', false); const metadataBitstreams: MetadataBitstream[] = [metadatabitstream]; const bitstreamStream = new BehaviorSubject(metadataBitstreams); @@ -63,7 +70,9 @@ describe('ClarinFilesSectionComponent', () => { 'getMetadataBitstream': of(bitstreamStream) } ); - halService = Object.assign(new HALEndpointServiceStub('some url')); + halService = Object.assign(new HALEndpointServiceStub('some url'), { + getRootHref: () => ROOT_HREF + }); await TestBed.configureTestingModule({ declarations: [ ClarinFilesSectionComponent ], @@ -88,4 +97,103 @@ describe('ClarinFilesSectionComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('generateCurlCommand', () => { + const BASE = `${ROOT_HREF}/core/bitstreams/handle`; + + it('should generate a curl command for a single file', () => { + component.itemHandle = '123456789/1'; + component.listOfFiles.next([createMetadataBitstream('simple.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "simple.txt" "${BASE}/123456789/1/simple.txt"` + ); + }); + + it('should generate a curl command for multiple files', () => { + component.itemHandle = '123456789/2'; + component.listOfFiles.next([ + createMetadataBitstream('file1.txt'), + createMetadataBitstream('file2.txt'), + ]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "file1.txt" "${BASE}/123456789/2/file1.txt" -o "file2.txt" "${BASE}/123456789/2/file2.txt"` + ); + }); + + it('should percent-encode spaces in URL but keep real name in -o', () => { + component.itemHandle = '123456789/3'; + component.listOfFiles.next([createMetadataBitstream('my file.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "my file.txt" "${BASE}/123456789/3/my%20file.txt"` + ); + }); + + it('should percent-encode parentheses in URL but keep real name in -o', () => { + component.itemHandle = '123456789/4'; + component.listOfFiles.next([createMetadataBitstream('logo (2).png')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "logo (2).png" "${BASE}/123456789/4/logo%20%282%29.png"` + ); + }); + + it('should percent-encode plus signs in URL but keep real name in -o', () => { + component.itemHandle = '123456789/5'; + component.listOfFiles.next([createMetadataBitstream('dtq+logo.png')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "dtq+logo.png" "${BASE}/123456789/5/dtq%2Blogo.png"` + ); + }); + + it('should handle mixed special characters in multiple files', () => { + component.itemHandle = '123456789/6'; + component.listOfFiles.next([ + createMetadataBitstream('dtq+logo (2).png'), + createMetadataBitstream('Screenshot 1.png'), + ]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "dtq+logo (2).png" "${BASE}/123456789/6/dtq%2Blogo%20%282%29.png" ` + + `-o "Screenshot 1.png" "${BASE}/123456789/6/Screenshot%201.png"` + ); + }); + + it('should preserve UTF-8 characters in -o filename', () => { + component.itemHandle = '123456789/9'; + component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (3).jfif')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9/M%C3%A9di%C3%A1%20%283%29.jfif"` + ); + }); + + it('should escape double quotes in filenames', () => { + component.itemHandle = '123456789/10'; + component.listOfFiles.next([createMetadataBitstream('file "quoted".txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10/file%20%22quoted%22.txt"` + ); + }); + + it('should set canShowCurlDownload to true when any file canPreview', () => { + component.canShowCurlDownload = false; + component.itemHandle = '123456789/7'; + component.listOfFiles.next([createMetadataBitstream('file.txt', true)]); + component.generateCurlCommand(); + expect(component.canShowCurlDownload).toBeTrue(); + }); + + it('should not set canShowCurlDownload for non-previewable files', () => { + component.canShowCurlDownload = false; + component.itemHandle = '123456789/8'; + component.listOfFiles.next([createMetadataBitstream('file.txt', false)]); + component.generateCurlCommand(); + expect(component.canShowCurlDownload).toBeFalse(); + }); + }); }); diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 9324c430df4..567850fab4f 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -9,6 +9,7 @@ import { Router } from '@angular/router'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { BehaviorSubject } from 'rxjs'; +import { encodeRFC3986URIComponent } from '../../shared/clarin-shared-util'; @Component({ selector: 'ds-clarin-files-section', @@ -108,20 +109,17 @@ export class ClarinFilesSectionComponent implements OnInit { return file.name; }); - // Generate curl command for individual bitstream downloads by handle + filename. - // Uses the backend endpoint: /api/core/bitstreams/handle/{prefix}/{suffix}/{filename} - // -J tells curl to use the filename from the Content-Disposition header (the real name) - // instead of the percent-encoded URL path. + // Generate curl command using -o "filename" "url" pairs. + // This avoids curl -J (Content-Disposition), which cannot create files + // with non-ASCII characters on Windows due to code-page limitations. const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; - const fileNamesFormatted = fileNames.map(fileName => `/${this.encodeFilenameForUrl(fileName)}`).join(','); - this.command = `curl -OJ "${baseUrl}{${fileNamesFormatted}}"`; - } - - /** - * Encode a filename for use in a URL path segment using the shared RFC3986 utility. - */ - private encodeFilenameForUrl(filename: string): string { - return encodeRFC3986URIComponent(filename); + const parts = fileNames.map(name => { + const url = `${baseUrl}/${encodeRFC3986URIComponent(name)}`; + // Escape backslashes and double quotes for safe use inside double-quoted shell strings + const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + return `-o "${safeName}" "${url}"`; + }); + this.command = `curl ${parts.join(' ')}`; } loadDownloadZipConfigProperties() { From d76a27176ac40a7dacbee6f4ae761d0e060fe49a Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 24 Feb 2026 16:25:46 +0100 Subject: [PATCH 08/15] fix: use inline encodeURIComponent instead of encodeRFC3986URIComponent encodeRFC3986URIComponent calls decodeURIComponent first, which throws URIError on filenames containing a literal percent sign (e.g. '100% done.txt') because '%' followed by non-hex chars is not a valid escape sequence. Replaced with inline encodeURIComponent() + parentheses encoding directly on the raw filename. Added test for literal percent sign in filenames. --- .../clarin-files-section.component.spec.ts | 9 +++++++++ .../clarin-files-section.component.ts | 9 ++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts index 83366391ea8..ffff9621fe9 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts @@ -195,5 +195,14 @@ describe('ClarinFilesSectionComponent', () => { component.generateCurlCommand(); expect(component.canShowCurlDownload).toBeFalse(); }); + + it('should handle filenames containing a literal percent sign', () => { + component.itemHandle = '123456789/11'; + component.listOfFiles.next([createMetadataBitstream('100% done.txt')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "100% done.txt" "${BASE}/123456789/11/100%25%20done.txt"` + ); + }); }); }); diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 567850fab4f..3e8dd505f83 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -1,5 +1,4 @@ import { Component, Input, OnInit } from '@angular/core'; -import { encodeRFC3986URIComponent } from '../../shared/clarin-shared-util'; import { Item } from '../../core/shared/item.model'; import { getAllSucceededRemoteListPayload, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getItemPageRoute } from '../item-page-routing-paths'; @@ -9,7 +8,6 @@ import { Router } from '@angular/router'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { BehaviorSubject } from 'rxjs'; -import { encodeRFC3986URIComponent } from '../../shared/clarin-shared-util'; @Component({ selector: 'ds-clarin-files-section', @@ -114,7 +112,12 @@ export class ClarinFilesSectionComponent implements OnInit { // with non-ASCII characters on Windows due to code-page limitations. const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; const parts = fileNames.map(name => { - const url = `${baseUrl}/${encodeRFC3986URIComponent(name)}`; + // Encode the raw filename for use in a URL path segment. + // Do NOT use encodeRFC3986URIComponent here — it calls decodeURIComponent first, + // which throws URIError on filenames containing literal '%' followed by non-hex chars. + const encodedName = encodeURIComponent(name) + .replace(/[()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); + const url = `${baseUrl}/${encodedName}`; // Escape backslashes and double quotes for safe use inside double-quoted shell strings const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); return `-o "${safeName}" "${url}"`; From d0fb750380bfe1dd4704b75a6b557cde7e0cb944 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 24 Feb 2026 16:46:21 +0100 Subject: [PATCH 09/15] fix: restore brace expansion {} in curl URL with -o for filenames curl command now uses brace expansion for compact URL: curl -o file1 -o file2 baseUrl{/encoded1,/encoded2} This combines: - {} brace expansion in the URL (compact, one URL for all files) - -o flags with real filenames (handles UTF-8 correctly via shell) --- .../clarin-files-section.component.spec.ts | 28 +++++++++---------- .../clarin-files-section.component.ts | 25 ++++++++--------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts index ffff9621fe9..b956128d10b 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts @@ -101,16 +101,16 @@ describe('ClarinFilesSectionComponent', () => { describe('generateCurlCommand', () => { const BASE = `${ROOT_HREF}/core/bitstreams/handle`; - it('should generate a curl command for a single file', () => { + it('should generate a curl command for a single file with brace expansion', () => { component.itemHandle = '123456789/1'; component.listOfFiles.next([createMetadataBitstream('simple.txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "simple.txt" "${BASE}/123456789/1/simple.txt"` + `curl -o "simple.txt" "${BASE}/123456789/1{/simple.txt}"` ); }); - it('should generate a curl command for multiple files', () => { + it('should generate a curl command for multiple files with brace expansion', () => { component.itemHandle = '123456789/2'; component.listOfFiles.next([ createMetadataBitstream('file1.txt'), @@ -118,7 +118,7 @@ describe('ClarinFilesSectionComponent', () => { ]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "file1.txt" "${BASE}/123456789/2/file1.txt" -o "file2.txt" "${BASE}/123456789/2/file2.txt"` + `curl -o "file1.txt" -o "file2.txt" "${BASE}/123456789/2{/file1.txt,/file2.txt}"` ); }); @@ -127,7 +127,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('my file.txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "my file.txt" "${BASE}/123456789/3/my%20file.txt"` + `curl -o "my file.txt" "${BASE}/123456789/3{/my%20file.txt}"` ); }); @@ -136,16 +136,16 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('logo (2).png')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "logo (2).png" "${BASE}/123456789/4/logo%20%282%29.png"` + `curl -o "logo (2).png" "${BASE}/123456789/4{/logo%20%282%29.png}"` ); }); - it('should percent-encode plus signs in URL but keep real name in -o', () => { + it('should percent-encode plus signs in URL', () => { component.itemHandle = '123456789/5'; component.listOfFiles.next([createMetadataBitstream('dtq+logo.png')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "dtq+logo.png" "${BASE}/123456789/5/dtq%2Blogo.png"` + `curl -o "dtq+logo.png" "${BASE}/123456789/5{/dtq%2Blogo.png}"` ); }); @@ -157,17 +157,17 @@ describe('ClarinFilesSectionComponent', () => { ]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "dtq+logo (2).png" "${BASE}/123456789/6/dtq%2Blogo%20%282%29.png" ` + - `-o "Screenshot 1.png" "${BASE}/123456789/6/Screenshot%201.png"` + `curl -o "dtq+logo (2).png" -o "Screenshot 1.png" ` + + `"${BASE}/123456789/6{/dtq%2Blogo%20%282%29.png,/Screenshot%201.png}"` ); }); - it('should preserve UTF-8 characters in -o filename', () => { + it('should preserve UTF-8 characters in -o filename and encode in URL', () => { component.itemHandle = '123456789/9'; component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (3).jfif')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9/M%C3%A9di%C3%A1%20%283%29.jfif"` + `curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9{/M%C3%A9di%C3%A1%20%283%29.jfif}"` ); }); @@ -176,7 +176,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('file "quoted".txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10/file%20%22quoted%22.txt"` + `curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10{/file%20%22quoted%22.txt}"` ); }); @@ -201,7 +201,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('100% done.txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "100% done.txt" "${BASE}/123456789/11/100%25%20done.txt"` + `curl -o "100% done.txt" "${BASE}/123456789/11{/100%25%20done.txt}"` ); }); }); diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 3e8dd505f83..54d03d3916f 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -107,22 +107,21 @@ export class ClarinFilesSectionComponent implements OnInit { return file.name; }); - // Generate curl command using -o "filename" "url" pairs. - // This avoids curl -J (Content-Disposition), which cannot create files - // with non-ASCII characters on Windows due to code-page limitations. + // Generate curl command with brace expansion in the URL and -o flags for correct filenames. + // Brace expansion: curl expands {/a,/b} into two requests to baseUrl/a and baseUrl/b. + // -o flags: one per file, giving curl the real filename to save as (handles UTF-8 correctly + // because the shell passes it directly, unlike -J which depends on Content-Disposition parsing). const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; - const parts = fileNames.map(name => { - // Encode the raw filename for use in a URL path segment. - // Do NOT use encodeRFC3986URIComponent here — it calls decodeURIComponent first, - // which throws URIError on filenames containing literal '%' followed by non-hex chars. - const encodedName = encodeURIComponent(name) + const encodedPaths = fileNames.map(name => { + return '/' + encodeURIComponent(name) .replace(/[()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); - const url = `${baseUrl}/${encodedName}`; - // Escape backslashes and double quotes for safe use inside double-quoted shell strings - const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); - return `-o "${safeName}" "${url}"`; }); - this.command = `curl ${parts.join(' ')}`; + const outputFlags = fileNames.map(name => { + const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + return `-o "${safeName}"`; + }).join(' '); + const braceList = encodedPaths.join(','); + this.command = `curl ${outputFlags} "${baseUrl}{${braceList}}"`; } loadDownloadZipConfigProperties() { From 640fdcbbf7c3bebcbd11702be3951b0467a9419b Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 24 Feb 2026 16:53:46 +0100 Subject: [PATCH 10/15] test: add complex filename test (diacritics, plus, hash, unmatched paren) New FE test for 'Media (+)#9) ano' verifying correct URL encoding in brace expansion and real filename in -o flag. --- .../clarin-files-section.component.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts index b956128d10b..f69c4824e0f 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts @@ -204,5 +204,14 @@ describe('ClarinFilesSectionComponent', () => { `curl -o "100% done.txt" "${BASE}/123456789/11{/100%25%20done.txt}"` ); }); + + it('should handle complex filename with diacritics, plus, hash, and unmatched paren', () => { + component.itemHandle = '123456789/12'; + component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (+)#9) ano')]); + component.generateCurlCommand(); + expect(component.command).toBe( + `curl -o "M\u00e9di\u00e1 (+)#9) ano" "${BASE}/123456789/12{/M%C3%A9di%C3%A1%20%28%2B%29%239%29%20ano}"` + ); + }); }); }); From dd17a54e69e514b7585a0a938ba4036df8d90df5 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 24 Feb 2026 17:16:47 +0100 Subject: [PATCH 11/15] fix: use separate -o url pairs instead of curl brace expansion curl URL globbing ({}) does NOT support per-file -o flags. When using curl -o f1 -o f2 url{/a,/b} curl maps the -o flags to URL arguments, not to globbed expansions, resulting in 'Got more output options than URLs' and only one file saved. Changed to separate -o + URL pairs per file: curl -o file1 url/file1 -o file2 url/file2 Updated all 12 test expectations to match. --- .../clarin-files-section.component.spec.ts | 27 ++++++++++--------- .../clarin-files-section.component.ts | 22 +++++++-------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts index f69c4824e0f..03c5cdd4779 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.spec.ts @@ -101,16 +101,16 @@ describe('ClarinFilesSectionComponent', () => { describe('generateCurlCommand', () => { const BASE = `${ROOT_HREF}/core/bitstreams/handle`; - it('should generate a curl command for a single file with brace expansion', () => { + it('should generate a curl command for a single file', () => { component.itemHandle = '123456789/1'; component.listOfFiles.next([createMetadataBitstream('simple.txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "simple.txt" "${BASE}/123456789/1{/simple.txt}"` + `curl -o "simple.txt" "${BASE}/123456789/1/simple.txt"` ); }); - it('should generate a curl command for multiple files with brace expansion', () => { + it('should generate a curl command for multiple files', () => { component.itemHandle = '123456789/2'; component.listOfFiles.next([ createMetadataBitstream('file1.txt'), @@ -118,7 +118,8 @@ describe('ClarinFilesSectionComponent', () => { ]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "file1.txt" -o "file2.txt" "${BASE}/123456789/2{/file1.txt,/file2.txt}"` + `curl -o "file1.txt" "${BASE}/123456789/2/file1.txt" ` + + `-o "file2.txt" "${BASE}/123456789/2/file2.txt"` ); }); @@ -127,7 +128,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('my file.txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "my file.txt" "${BASE}/123456789/3{/my%20file.txt}"` + `curl -o "my file.txt" "${BASE}/123456789/3/my%20file.txt"` ); }); @@ -136,7 +137,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('logo (2).png')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "logo (2).png" "${BASE}/123456789/4{/logo%20%282%29.png}"` + `curl -o "logo (2).png" "${BASE}/123456789/4/logo%20%282%29.png"` ); }); @@ -145,7 +146,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('dtq+logo.png')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "dtq+logo.png" "${BASE}/123456789/5{/dtq%2Blogo.png}"` + `curl -o "dtq+logo.png" "${BASE}/123456789/5/dtq%2Blogo.png"` ); }); @@ -157,8 +158,8 @@ describe('ClarinFilesSectionComponent', () => { ]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "dtq+logo (2).png" -o "Screenshot 1.png" ` + - `"${BASE}/123456789/6{/dtq%2Blogo%20%282%29.png,/Screenshot%201.png}"` + `curl -o "dtq+logo (2).png" "${BASE}/123456789/6/dtq%2Blogo%20%282%29.png" ` + + `-o "Screenshot 1.png" "${BASE}/123456789/6/Screenshot%201.png"` ); }); @@ -167,7 +168,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (3).jfif')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9{/M%C3%A9di%C3%A1%20%283%29.jfif}"` + `curl -o "M\u00e9di\u00e1 (3).jfif" "${BASE}/123456789/9/M%C3%A9di%C3%A1%20%283%29.jfif"` ); }); @@ -176,7 +177,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('file "quoted".txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10{/file%20%22quoted%22.txt}"` + `curl -o "file \\"quoted\\".txt" "${BASE}/123456789/10/file%20%22quoted%22.txt"` ); }); @@ -201,7 +202,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('100% done.txt')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "100% done.txt" "${BASE}/123456789/11{/100%25%20done.txt}"` + `curl -o "100% done.txt" "${BASE}/123456789/11/100%25%20done.txt"` ); }); @@ -210,7 +211,7 @@ describe('ClarinFilesSectionComponent', () => { component.listOfFiles.next([createMetadataBitstream('M\u00e9di\u00e1 (+)#9) ano')]); component.generateCurlCommand(); expect(component.command).toBe( - `curl -o "M\u00e9di\u00e1 (+)#9) ano" "${BASE}/123456789/12{/M%C3%A9di%C3%A1%20%28%2B%29%239%29%20ano}"` + `curl -o "M\u00e9di\u00e1 (+)#9) ano" "${BASE}/123456789/12/M%C3%A9di%C3%A1%20%28%2B%29%239%29%20ano"` ); }); }); diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 54d03d3916f..88dd032ea26 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -107,21 +107,19 @@ export class ClarinFilesSectionComponent implements OnInit { return file.name; }); - // Generate curl command with brace expansion in the URL and -o flags for correct filenames. - // Brace expansion: curl expands {/a,/b} into two requests to baseUrl/a and baseUrl/b. - // -o flags: one per file, giving curl the real filename to save as (handles UTF-8 correctly - // because the shell passes it directly, unlike -J which depends on Content-Disposition parsing). + // Generate curl command with -o "filename" "url" pairs for each file. + // Each file needs its own -o + URL pair because curl URL globbing ({}) + // does NOT support per-file -o flags (multiple -o with {} results in + // "Got more output options than URLs" and only the first file is saved). + // Using -o lets the shell pass the real filename (including UTF-8) directly. const baseUrl = `${this.halService.getRootHref()}/core/bitstreams/handle/${this.itemHandle}`; - const encodedPaths = fileNames.map(name => { - return '/' + encodeURIComponent(name) + const parts = fileNames.map(name => { + const encodedName = encodeURIComponent(name) .replace(/[()]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase()); - }); - const outputFlags = fileNames.map(name => { const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); - return `-o "${safeName}"`; - }).join(' '); - const braceList = encodedPaths.join(','); - this.command = `curl ${outputFlags} "${baseUrl}{${braceList}}"`; + return `-o "${safeName}" "${baseUrl}/${encodedName}"`; + }); + this.command = `curl ${parts.join(' ')}`; } loadDownloadZipConfigProperties() { From f6a38c4512880c08d8938f52aae1aa143a676e29 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 24 Feb 2026 17:22:37 +0100 Subject: [PATCH 12/15] feat: show curl command in modal dialog with copy button Replace inline command display with a centered NgbModal (size: lg) that shows the curl command in a scrollable pre block. Includes a copy-to-clipboard button with visual feedback (checkmark + 'Copied!' for 2s). - Added NgbModal injection and openCommandModal()/copyCommand() methods - Removed old isCommandLineVisible toggle and #command-div hover styles - Added i18n keys for en, cs, de (copy/copied/close) - Updated spec to import NgbModalModule --- .../clarin-files-section.component.html | 26 +++++++++++-- .../clarin-files-section.component.scss | 39 +++---------------- .../clarin-files-section.component.spec.ts | 4 +- .../clarin-files-section.component.ts | 20 +++++++--- src/assets/i18n/cs.json5 | 3 ++ src/assets/i18n/de.json5 | 3 ++ src/assets/i18n/en.json5 | 3 ++ 7 files changed, 55 insertions(+), 43 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.html b/src/app/item-page/clarin-files-section/clarin-files-section.component.html index 09829decf58..c109c0cc29f 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.html +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.html @@ -3,15 +3,33 @@
 {{'item.page.files.head' | translate}}
-   {{'item.page.download.button.command.line' | translate}} -
-
{{ command }}
-
+ + + + + + { let component: ClarinFilesSectionComponent; @@ -77,7 +78,8 @@ describe('ClarinFilesSectionComponent', () => { await TestBed.configureTestingModule({ declarations: [ ClarinFilesSectionComponent ], imports: [ - TranslateModule.forRoot() + TranslateModule.forRoot(), + NgbModalModule, ], providers: [ { provide: RegistryService, useValue: mockRegistryService }, diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts index 88dd032ea26..e95b0cd4c44 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.ts +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.ts @@ -8,6 +8,7 @@ import { Router } from '@angular/router'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { BehaviorSubject } from 'rxjs'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'ds-clarin-files-section', @@ -29,9 +30,9 @@ export class ClarinFilesSectionComponent implements OnInit { canShowCurlDownload = false; /** - * If download by command button is click, the command line will be shown + * Whether the command was recently copied to clipboard */ - isCommandLineVisible = false; + commandCopied = false; /** * command for the download command feature @@ -75,7 +76,8 @@ export class ClarinFilesSectionComponent implements OnInit { constructor(protected registryService: RegistryService, protected router: Router, protected halService: HALEndpointService, - protected configurationService: ConfigurationDataService) { + protected configurationService: ConfigurationDataService, + protected modalService: NgbModal) { } ngOnInit(): void { @@ -90,8 +92,16 @@ export class ClarinFilesSectionComponent implements OnInit { this.loadDownloadZipConfigProperties(); } - setCommandline() { - this.isCommandLineVisible = !this.isCommandLineVisible; + openCommandModal(content: any) { + this.commandCopied = false; + this.modalService.open(content, { size: 'lg', centered: true }); + } + + copyCommand() { + navigator.clipboard.writeText(this.command).then(() => { + this.commandCopied = true; + setTimeout(() => this.commandCopied = false, 2000); + }); } downloadFiles() { diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 635847a464a..bc46ac9a259 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -9203,6 +9203,9 @@ // "item.page.download.button.command.line": "Download instructions for command line", "item.page.download.button.command.line": "Instrukce pro stažení z příkazové řádky", + "item.page.download.command.copy": "Kopírovat do schránky", + "item.page.download.command.copied": "Zkopírováno!", + "item.page.download.command.close": "Zavřít", // "item.page.download.button.all.files.zip": "Download all files in item", "item.page.download.button.all.files.zip": "Stáhnout všechny soubory záznamu", diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 041295408d5..b1ab4b24764 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -9308,6 +9308,9 @@ // "item.page.download.button.command.line": "Download instructions for command line", // TODO New key - Add a translation "item.page.download.button.command.line": "Download instructions for command line", + "item.page.download.command.copy": "Copy to clipboard", + "item.page.download.command.copied": "Copied!", + "item.page.download.command.close": "Close", // "item.page.download.button.all.files.zip": "Download all files in item", // TODO New key - Add a translation diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 77e9cb4497d..f2460aa3969 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6099,6 +6099,9 @@ "item.page.files.head": "Files in this item", "item.page.download.button.command.line": "Download instructions for command line", + "item.page.download.command.copy": "Copy to clipboard", + "item.page.download.command.copied": "Copied!", + "item.page.download.command.close": "Close", "item.page.download.button.all.files.zip": "Download all files in item", From 3eed3482d90faaa482666d0b5f6fa4fd1842f733 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 25 Feb 2026 08:32:20 +0100 Subject: [PATCH 13/15] Revert unnecessary changes --- src/assets/i18n/cs.json5 | 2 +- src/assets/i18n/de.json5 | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index bc46ac9a259..937c3706e2a 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -9203,7 +9203,7 @@ // "item.page.download.button.command.line": "Download instructions for command line", "item.page.download.button.command.line": "Instrukce pro stažení z příkazové řádky", - "item.page.download.command.copy": "Kopírovat do schránky", + "item.page.download.command.copy": "Kopírovat", "item.page.download.command.copied": "Zkopírováno!", "item.page.download.command.close": "Zavřít", diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index b1ab4b24764..041295408d5 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -9308,9 +9308,6 @@ // "item.page.download.button.command.line": "Download instructions for command line", // TODO New key - Add a translation "item.page.download.button.command.line": "Download instructions for command line", - "item.page.download.command.copy": "Copy to clipboard", - "item.page.download.command.copied": "Copied!", - "item.page.download.command.close": "Close", // "item.page.download.button.all.files.zip": "Download all files in item", // TODO New key - Add a translation From c10eb31bf0af854585842be46f4d1ebf207b8267 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 25 Feb 2026 14:11:10 +0100 Subject: [PATCH 14/15] Address Copilot review suggestions: accessibility, security, test fixes - Reset canShowCurlDownload at start of generateCurlCommand() - Add aria-labelledby to modal for screen reader accessibility - Add .catch() to navigator.clipboard.writeText() for error handling - Escape dollar signs and backticks in filenames for shell safety - Fix ConfigurationDataService mock to return RemoteData-shaped object - Add tests for canShowCurlDownload reset and shell injection protection --- .../clarin-files-section.component.html | 2 +- .../clarin-files-section.component.spec.ts | 22 ++++++++++++++++++- .../clarin-files-section.component.ts | 8 +++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/clarin-files-section/clarin-files-section.component.html b/src/app/item-page/clarin-files-section/clarin-files-section.component.html index c109c0cc29f..72d393fbd24 100644 --- a/src/app/item-page/clarin-files-section/clarin-files-section.component.html +++ b/src/app/item-page/clarin-files-section/clarin-files-section.component.html @@ -12,7 +12,7 @@
 {{'item.page.files.head' | translate}}<