Compare commits

...

6 Commits

Author SHA1 Message Date
Dan Brown
7111e080c1 Updated version and assets for release v26.03.1 2026-03-17 11:00:48 +00:00
Dan Brown
ee4786f83a Merge branch 'development' into release 2026-03-17 10:59:56 +00:00
Dan Brown
0120b475eb Deps: Updated PHP deps pre v26.03.1 2026-03-17 10:59:11 +00:00
Dan Brown
8a59895ba0 Merge branch 'sec_chapter_export' into development 2026-03-17 10:41:51 +00:00
Dan Brown
a9ffd3e0c7 Responses: Added extra sanitization for download names
From testing, don't think this could exploited directly, as the response
would error instead of allowing control characters, but this adds an
extra layer of sanitization, and switches to encoded disposition
filenames for better UTF8 support.
2026-03-16 18:28:44 +00:00
Dan Brown
f4c9d2b049 Exports: Fixed scope of pages in chapter MD export
Added tests to cover children of all MD exports
2026-03-13 13:35:28 +00:00
11 changed files with 125 additions and 61 deletions

View File

@@ -323,7 +323,7 @@ class ExportFormatter
$text .= $description . "\n\n";
}
foreach ($chapter->pages as $page) {
foreach ($chapter->getVisiblePages() as $page) {
$text .= $this->pageToMarkdown($page) . "\n\n";
}

View File

@@ -102,12 +102,15 @@ class DownloadResponseFactory
protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array
{
$disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
$downloadName = str_replace('"', '', $fileName);
$downloadName = str_replace(['"', '/', '\\', '$'], '', $fileName);
$downloadName = preg_replace('/[\x00-\x1F\x7F]/', '', $downloadName);
$encodedDownloadName = rawurlencode($downloadName);
return [
'Content-Type' => $mime,
'Content-Length' => $fileSize,
'Content-Disposition' => "{$disposition}; filename=\"{$downloadName}\"",
'Content-Disposition' => "{$disposition}; filename*=UTF-8''{$encodedDownloadName}",
'X-Content-Type-Options' => 'nosniff',
];
}

58
composer.lock generated
View File

@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.373.2",
"version": "3.373.3",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "483fba51c28b3a0c0647bf5100e0edca82090b18"
"reference": "d23edc4cf9cd81cb98b5beb9c1fb3737f535b1e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/483fba51c28b3a0c0647bf5100e0edca82090b18",
"reference": "483fba51c28b3a0c0647bf5100e0edca82090b18",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d23edc4cf9cd81cb98b5beb9c1fb3737f535b1e5",
"reference": "d23edc4cf9cd81cb98b5beb9c1fb3737f535b1e5",
"shasum": ""
},
"require": {
@@ -153,22 +153,22 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.373.2"
"source": "https://github.com/aws/aws-sdk-php/tree/3.373.3"
},
"time": "2026-03-13T18:08:30+00:00"
"time": "2026-03-16T18:15:27+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "v3.0.3",
"version": "v3.0.4",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563"
"reference": "3feed0e212b8412cc5d2612706744789b0615824"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563",
"reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/3feed0e212b8412cc5d2612706744789b0615824",
"reference": "3feed0e212b8412cc5d2612706744789b0615824",
"shasum": ""
},
"require": {
@@ -208,9 +208,9 @@
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3"
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.4"
},
"time": "2025-11-19T17:15:36+00:00"
"time": "2026-03-16T01:01:30+00:00"
},
{
"name": "brick/math",
@@ -2943,20 +2943,20 @@
},
{
"name": "league/uri",
"version": "7.8.0",
"version": "7.8.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
"reference": "4436c6ec8d458e4244448b069cc572d088230b76"
"reference": "08cf38e3924d4f56238125547b5720496fac8fd4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76",
"reference": "4436c6ec8d458e4244448b069cc572d088230b76",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4",
"reference": "08cf38e3924d4f56238125547b5720496fac8fd4",
"shasum": ""
},
"require": {
"league/uri-interfaces": "^7.8",
"league/uri-interfaces": "^7.8.1",
"php": "^8.1",
"psr/http-factory": "^1"
},
@@ -3029,7 +3029,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri/tree/7.8.0"
"source": "https://github.com/thephpleague/uri/tree/7.8.1"
},
"funding": [
{
@@ -3037,20 +3037,20 @@
"type": "github"
}
],
"time": "2026-01-14T17:24:56+00:00"
"time": "2026-03-15T20:22:25+00:00"
},
{
"name": "league/uri-interfaces",
"version": "7.8.0",
"version": "7.8.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
"reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4"
"reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4",
"reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928",
"reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928",
"shasum": ""
},
"require": {
@@ -3113,7 +3113,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0"
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1"
},
"funding": [
{
@@ -3121,7 +3121,7 @@
"type": "github"
}
],
"time": "2026-01-15T06:54:53+00:00"
"time": "2026-03-08T20:05:35+00:00"
},
{
"name": "masterminds/html5",
@@ -9169,11 +9169,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.40",
"version": "2.1.41",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/a2eae8f20856b3afe74bf1f9726ce8c11438e300",
"reference": "a2eae8f20856b3afe74bf1f9726ce8c11438e300",
"shasum": ""
},
"require": {
@@ -9218,7 +9218,7 @@
"type": "github"
}
],
"time": "2026-02-23T15:04:35+00:00"
"time": "2026-03-16T18:24:10+00:00"
},
{
"name": "phpunit/php-code-coverage",

View File

@@ -1 +1 @@
15574ddf174d8f2b0efbdcbc6dbfcd907eb41593821d4f06d47e74822a80a394
df9854136249aa316fa799eacb046eb3c14a6248fe5f3038ab7f500e83e83866

View File

@@ -19,7 +19,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/books/{$book->id}/export/html");
$resp->assertStatus(200);
$resp->assertSee($book->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.html');
}
public function test_book_plain_text_endpoint()
@@ -30,7 +30,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/books/{$book->id}/export/plaintext");
$resp->assertStatus(200);
$resp->assertSee($book->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.txt');
}
public function test_book_pdf_endpoint()
@@ -40,7 +40,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/books/{$book->id}/export/pdf");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.pdf');
}
public function test_book_markdown_endpoint()
@@ -50,7 +50,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/books/{$book->id}/export/markdown");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.md');
$resp->assertSee('# ' . $book->name);
$resp->assertSee('# ' . $book->pages()->first()->name);
$resp->assertSee('# ' . $book->chapters()->first()->name);
@@ -63,7 +63,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/books/{$book->id}/export/zip");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.zip"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.zip');
$zip = ZipTestHelper::extractFromZipResponse($resp);
$this->assertArrayHasKey('book', $zip->data);
@@ -77,7 +77,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/chapters/{$chapter->id}/export/html");
$resp->assertStatus(200);
$resp->assertSee($chapter->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.html');
}
public function test_chapter_plain_text_endpoint()
@@ -88,7 +88,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/chapters/{$chapter->id}/export/plaintext");
$resp->assertStatus(200);
$resp->assertSee($chapter->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.txt');
}
public function test_chapter_pdf_endpoint()
@@ -98,7 +98,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/chapters/{$chapter->id}/export/pdf");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.pdf');
}
public function test_chapter_markdown_endpoint()
@@ -108,7 +108,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/chapters/{$chapter->id}/export/markdown");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.md"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.md');
$resp->assertSee('# ' . $chapter->name);
$resp->assertSee('# ' . $chapter->pages()->first()->name);
}
@@ -120,7 +120,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/chapters/{$chapter->id}/export/zip");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.zip"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.zip');
$zip = ZipTestHelper::extractFromZipResponse($resp);
$this->assertArrayHasKey('chapter', $zip->data);
@@ -134,7 +134,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/pages/{$page->id}/export/html");
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.html');
}
public function test_page_plain_text_endpoint()
@@ -145,7 +145,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/pages/{$page->id}/export/plaintext");
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.txt');
}
public function test_page_pdf_endpoint()
@@ -155,7 +155,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/pages/{$page->id}/export/pdf");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.pdf');
}
public function test_page_markdown_endpoint()
@@ -166,7 +166,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/pages/{$page->id}/export/markdown");
$resp->assertStatus(200);
$resp->assertSee('# ' . $page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.md');
}
public function test_page_zip_endpoint()
@@ -176,7 +176,7 @@ class ExportsApiTest extends TestCase
$resp = $this->get("/api/pages/{$page->id}/export/zip");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.zip"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.zip');
$zip = ZipTestHelper::extractFromZipResponse($resp);
$this->assertArrayHasKey('page', $zip->data);

View File

@@ -18,7 +18,7 @@ class HtmlExportTest extends TestCase
$resp = $this->get($page->getUrl('/export/html'));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.html');
}
public function test_book_html_export()
@@ -31,7 +31,7 @@ class HtmlExportTest extends TestCase
$resp->assertStatus(200);
$resp->assertSee($book->name);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.html');
}
public function test_book_html_export_shows_html_descriptions()
@@ -58,7 +58,7 @@ class HtmlExportTest extends TestCase
$resp->assertStatus(200);
$resp->assertSee($chapter->name);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.html');
}
public function test_chapter_html_export_shows_html_descriptions()

View File

@@ -14,7 +14,7 @@ class MarkdownExportTest extends TestCase
$resp = $this->asEditor()->get($page->getUrl('/export/markdown'));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.md');
}
public function test_page_markdown_export_uses_existing_markdown_if_apparent()
@@ -56,6 +56,20 @@ class MarkdownExportTest extends TestCase
$resp->assertSee('My **chapter** description');
}
public function test_chapter_markdown_export_pages_are_permission_controlled()
{
$chapter = $this->entities->chapterHasPages();
$page = $chapter->pages()->first();
$page->name = 'MyPageWhichShouldNotBeFound';
$page->save();
$this->permissions->disableEntityInheritedPermissions($page);
$resp = $this->asEditor()->get($chapter->getUrl('/export/markdown'));
$resp->assertSee('# ' . $chapter->name);
$resp->assertDontSee('MyPageWhichShouldNotBeFound');
}
public function test_book_markdown_export()
{
$book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
@@ -76,6 +90,38 @@ class MarkdownExportTest extends TestCase
$resp->assertSee('My **chapter** description');
}
public function test_book_markdown_export_chapters_are_permission_controlled()
{
$book = $this->entities->bookHasChaptersAndPages();
$chapter = $book->chapters()->first();
$page = $chapter->pages()->first();
$page->name = 'MyPageWhichShouldNotBeFound';
$page->save();
$chapter->name = 'MyChapterWhichShouldNotBeFound';
$chapter->save();
$this->permissions->disableEntityInheritedPermissions($chapter);
$resp = $this->asEditor()->get($book->getUrl('/export/markdown'));
$resp->assertSee('# ' . $book->name);
$resp->assertDontSee('MyChapterWhichShouldNotBeFound');
$resp->assertDontSee('MyPageWhichShouldNotBeFound');
}
public function test_book_markdown_export_direct_pages_are_permission_controlled()
{
$book = $this->entities->bookHasChaptersAndPages();
$page = $book->directPages()->first();
$page->name = 'MyPageWhichShouldNotBeFound';
$page->save();
$this->permissions->disableEntityInheritedPermissions($page);
$resp = $this->asEditor()->get($book->getUrl('/export/markdown'));
$resp->assertSee('# ' . $book->name);
$resp->assertDontSee('MyPageWhichShouldNotBeFound');
}
public function test_book_markdown_export_concats_immediate_pages_with_newlines()
{
/** @var Book $book */

View File

@@ -17,7 +17,7 @@ class PdfExportTest extends TestCase
$resp = $this->get($page->getUrl('/export/pdf'));
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.pdf');
}
public function test_book_pdf_export()
@@ -28,7 +28,7 @@ class PdfExportTest extends TestCase
$resp = $this->get($book->getUrl('/export/pdf'));
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.pdf');
}
public function test_chapter_pdf_export()
@@ -38,7 +38,7 @@ class PdfExportTest extends TestCase
$resp = $this->get($chapter->getUrl('/export/pdf'));
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.pdf');
}

View File

@@ -14,7 +14,7 @@ class TextExportTest extends TestCase
$resp = $this->get($page->getUrl('/export/plaintext'));
$resp->assertStatus(200);
$resp->assertSee($page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.txt');
}
public function test_book_text_export()
@@ -35,7 +35,7 @@ class TextExportTest extends TestCase
$resp->assertSee($directPage->name);
$resp->assertSee('My awesome page');
$resp->assertSee('My little nested page');
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.txt');
}
public function test_book_text_export_format()
@@ -68,7 +68,7 @@ class TextExportTest extends TestCase
$resp->assertSee($chapter->name);
$resp->assertSee($page->name);
$resp->assertSee('This is content within the page!');
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.txt');
}
public function test_chapter_text_export_format()

View File

@@ -324,7 +324,7 @@ class AttachmentTest extends TestCase
$attachmentGet = $this->get($attachment->getUrl(true));
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'upload_test_file.txt');
$attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
$this->files->deleteAllAttachmentFiles();
@@ -340,7 +340,22 @@ class AttachmentTest extends TestCase
$attachmentGet = $this->get($attachment->getUrl(true));
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'test_file.html');
$this->files->deleteAllAttachmentFiles();
}
public function test_file_access_name_in_content_disposition_header_is_sanitized()
{
$page = $this->entities->page();
$this->asAdmin();
$attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
$attachment->name = "my\\_/super\n_fu\$n_\tfile";
$attachment->save();
$attachmentGet = $this->get($attachment->getUrl(true));
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'my_super_fun_file.html');
$this->files->deleteAllAttachmentFiles();
}

View File

@@ -1 +1 @@
v26.03
v26.03.1