Compare commits

..

5 Commits

Author SHA1 Message Date
Théo Zehnder
a81d59029d Fix: New revision is created but no changes were made #6062 2026-03-19 14:49:14 +01: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
20 changed files with 142 additions and 206 deletions

6
.gitignore vendored
View File

@@ -8,10 +8,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
/public/css
/public/js
/public/bower
/public/build/
/public/favicon.ico

View File

@@ -118,27 +118,29 @@ class PageRepo
*/
public function update(Page $page, array $input): Page
{
// Hold the old details to compare later
// Hold the old details to compare later.
$oldName = $page->name;
$oldHtml = $page->html;
$oldMarkdown = $page->markdown;
$this->updateTemplateStatusAndContentFromInput($page, $input);
$page = $this->baseRepo->update($page, $input);
// Update with new details
$page->revision_count++;
$page->save();
// Remove all update drafts for this user and page.
$this->revisionRepo->deleteDraftsForCurrentUser($page);
// Save a revision after updating
// Values used to determine if a change has been made.
$summary = trim($input['summary'] ?? '');
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
$htmlChanged = isset($page->html) && $page->html !== $oldHtml;
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
$markdownChanged = isset($page->markdown) && $page->markdown !== $oldMarkdown;
// Update with new details, only if the page really changed.
if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {
$page = $this->baseRepo->update($page, $input);
$page->revision_count++;
$page->save();
// Remove all update drafts for this user and page.
$this->revisionRepo->deleteDraftsForCurrentUser($page);
$this->revisionRepo->storeNewForPage($page, $summary);
}

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
22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1

33
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

32
public/dist/code.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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.01-dev