Skip to content

Commit 8a59895

Browse files
committed
Merge branch 'sec_chapter_export' into development
2 parents 4f18fea + a9ffd3e commit 8a59895

File tree

8 files changed

+94
-30
lines changed

8 files changed

+94
-30
lines changed

app/Exports/ExportFormatter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ public function chapterToMarkdown(Chapter $chapter): string
323323
$text .= $description . "\n\n";
324324
}
325325

326-
foreach ($chapter->pages as $page) {
326+
foreach ($chapter->getVisiblePages() as $page) {
327327
$text .= $this->pageToMarkdown($page) . "\n\n";
328328
}
329329

app/Http/DownloadResponseFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,15 @@ public function streamedFileInline(string $filePath, ?string $fileName = null):
102102
protected function getHeaders(string $fileName, int $fileSize, string $mime = 'application/octet-stream'): array
103103
{
104104
$disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
105-
$downloadName = str_replace('"', '', $fileName);
105+
106+
$downloadName = str_replace(['"', '/', '\\', '$'], '', $fileName);
107+
$downloadName = preg_replace('/[\x00-\x1F\x7F]/', '', $downloadName);
108+
$encodedDownloadName = rawurlencode($downloadName);
106109

107110
return [
108111
'Content-Type' => $mime,
109112
'Content-Length' => $fileSize,
110-
'Content-Disposition' => "{$disposition}; filename=\"{$downloadName}\"",
113+
'Content-Disposition' => "{$disposition}; filename*=UTF-8''{$encodedDownloadName}",
111114
'X-Content-Type-Options' => 'nosniff',
112115
];
113116
}

tests/Api/ExportsApiTest.php

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function test_book_html_endpoint()
1919
$resp = $this->get("/api/books/{$book->id}/export/html");
2020
$resp->assertStatus(200);
2121
$resp->assertSee($book->name);
22-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
22+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.html');
2323
}
2424

2525
public function test_book_plain_text_endpoint()
@@ -30,7 +30,7 @@ public function test_book_plain_text_endpoint()
3030
$resp = $this->get("/api/books/{$book->id}/export/plaintext");
3131
$resp->assertStatus(200);
3232
$resp->assertSee($book->name);
33-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
33+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.txt');
3434
}
3535

3636
public function test_book_pdf_endpoint()
@@ -40,7 +40,7 @@ public function test_book_pdf_endpoint()
4040

4141
$resp = $this->get("/api/books/{$book->id}/export/pdf");
4242
$resp->assertStatus(200);
43-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
43+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.pdf');
4444
}
4545

4646
public function test_book_markdown_endpoint()
@@ -50,7 +50,7 @@ public function test_book_markdown_endpoint()
5050

5151
$resp = $this->get("/api/books/{$book->id}/export/markdown");
5252
$resp->assertStatus(200);
53-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
53+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.md');
5454
$resp->assertSee('# ' . $book->name);
5555
$resp->assertSee('# ' . $book->pages()->first()->name);
5656
$resp->assertSee('# ' . $book->chapters()->first()->name);
@@ -63,7 +63,7 @@ public function test_book_zip_endpoint()
6363

6464
$resp = $this->get("/api/books/{$book->id}/export/zip");
6565
$resp->assertStatus(200);
66-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.zip"');
66+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.zip');
6767

6868
$zip = ZipTestHelper::extractFromZipResponse($resp);
6969
$this->assertArrayHasKey('book', $zip->data);
@@ -77,7 +77,7 @@ public function test_chapter_html_endpoint()
7777
$resp = $this->get("/api/chapters/{$chapter->id}/export/html");
7878
$resp->assertStatus(200);
7979
$resp->assertSee($chapter->name);
80-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
80+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.html');
8181
}
8282

8383
public function test_chapter_plain_text_endpoint()
@@ -88,7 +88,7 @@ public function test_chapter_plain_text_endpoint()
8888
$resp = $this->get("/api/chapters/{$chapter->id}/export/plaintext");
8989
$resp->assertStatus(200);
9090
$resp->assertSee($chapter->name);
91-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
91+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.txt');
9292
}
9393

9494
public function test_chapter_pdf_endpoint()
@@ -98,7 +98,7 @@ public function test_chapter_pdf_endpoint()
9898

9999
$resp = $this->get("/api/chapters/{$chapter->id}/export/pdf");
100100
$resp->assertStatus(200);
101-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
101+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.pdf');
102102
}
103103

104104
public function test_chapter_markdown_endpoint()
@@ -108,7 +108,7 @@ public function test_chapter_markdown_endpoint()
108108

109109
$resp = $this->get("/api/chapters/{$chapter->id}/export/markdown");
110110
$resp->assertStatus(200);
111-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.md"');
111+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.md');
112112
$resp->assertSee('# ' . $chapter->name);
113113
$resp->assertSee('# ' . $chapter->pages()->first()->name);
114114
}
@@ -120,7 +120,7 @@ public function test_chapter_zip_endpoint()
120120

121121
$resp = $this->get("/api/chapters/{$chapter->id}/export/zip");
122122
$resp->assertStatus(200);
123-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.zip"');
123+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.zip');
124124

125125
$zip = ZipTestHelper::extractFromZipResponse($resp);
126126
$this->assertArrayHasKey('chapter', $zip->data);
@@ -134,7 +134,7 @@ public function test_page_html_endpoint()
134134
$resp = $this->get("/api/pages/{$page->id}/export/html");
135135
$resp->assertStatus(200);
136136
$resp->assertSee($page->name);
137-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
137+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.html');
138138
}
139139

140140
public function test_page_plain_text_endpoint()
@@ -145,7 +145,7 @@ public function test_page_plain_text_endpoint()
145145
$resp = $this->get("/api/pages/{$page->id}/export/plaintext");
146146
$resp->assertStatus(200);
147147
$resp->assertSee($page->name);
148-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
148+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.txt');
149149
}
150150

151151
public function test_page_pdf_endpoint()
@@ -155,7 +155,7 @@ public function test_page_pdf_endpoint()
155155

156156
$resp = $this->get("/api/pages/{$page->id}/export/pdf");
157157
$resp->assertStatus(200);
158-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
158+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.pdf');
159159
}
160160

161161
public function test_page_markdown_endpoint()
@@ -166,7 +166,7 @@ public function test_page_markdown_endpoint()
166166
$resp = $this->get("/api/pages/{$page->id}/export/markdown");
167167
$resp->assertStatus(200);
168168
$resp->assertSee('# ' . $page->name);
169-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
169+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.md');
170170
}
171171

172172
public function test_page_zip_endpoint()
@@ -176,7 +176,7 @@ public function test_page_zip_endpoint()
176176

177177
$resp = $this->get("/api/pages/{$page->id}/export/zip");
178178
$resp->assertStatus(200);
179-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.zip"');
179+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.zip');
180180

181181
$zip = ZipTestHelper::extractFromZipResponse($resp);
182182
$this->assertArrayHasKey('page', $zip->data);

tests/Exports/HtmlExportTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public function test_page_html_export()
1818
$resp = $this->get($page->getUrl('/export/html'));
1919
$resp->assertStatus(200);
2020
$resp->assertSee($page->name);
21-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
21+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.html');
2222
}
2323

2424
public function test_book_html_export()
@@ -31,7 +31,7 @@ public function test_book_html_export()
3131
$resp->assertStatus(200);
3232
$resp->assertSee($book->name);
3333
$resp->assertSee($page->name);
34-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
34+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.html');
3535
}
3636

3737
public function test_book_html_export_shows_html_descriptions()
@@ -58,7 +58,7 @@ public function test_chapter_html_export()
5858
$resp->assertStatus(200);
5959
$resp->assertSee($chapter->name);
6060
$resp->assertSee($page->name);
61-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.html"');
61+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.html');
6262
}
6363

6464
public function test_chapter_html_export_shows_html_descriptions()

tests/Exports/MarkdownExportTest.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function test_page_markdown_export()
1414
$resp = $this->asEditor()->get($page->getUrl('/export/markdown'));
1515
$resp->assertStatus(200);
1616
$resp->assertSee($page->name);
17-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
17+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.md');
1818
}
1919

2020
public function test_page_markdown_export_uses_existing_markdown_if_apparent()
@@ -56,6 +56,20 @@ public function test_chapter_markdown_export()
5656
$resp->assertSee('My **chapter** description');
5757
}
5858

59+
public function test_chapter_markdown_export_pages_are_permission_controlled()
60+
{
61+
$chapter = $this->entities->chapterHasPages();
62+
$page = $chapter->pages()->first();
63+
$page->name = 'MyPageWhichShouldNotBeFound';
64+
$page->save();
65+
$this->permissions->disableEntityInheritedPermissions($page);
66+
67+
$resp = $this->asEditor()->get($chapter->getUrl('/export/markdown'));
68+
69+
$resp->assertSee('# ' . $chapter->name);
70+
$resp->assertDontSee('MyPageWhichShouldNotBeFound');
71+
}
72+
5973
public function test_book_markdown_export()
6074
{
6175
$book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
@@ -76,6 +90,38 @@ public function test_book_markdown_export()
7690
$resp->assertSee('My **chapter** description');
7791
}
7892

93+
public function test_book_markdown_export_chapters_are_permission_controlled()
94+
{
95+
$book = $this->entities->bookHasChaptersAndPages();
96+
$chapter = $book->chapters()->first();
97+
$page = $chapter->pages()->first();
98+
$page->name = 'MyPageWhichShouldNotBeFound';
99+
$page->save();
100+
$chapter->name = 'MyChapterWhichShouldNotBeFound';
101+
$chapter->save();
102+
$this->permissions->disableEntityInheritedPermissions($chapter);
103+
104+
$resp = $this->asEditor()->get($book->getUrl('/export/markdown'));
105+
106+
$resp->assertSee('# ' . $book->name);
107+
$resp->assertDontSee('MyChapterWhichShouldNotBeFound');
108+
$resp->assertDontSee('MyPageWhichShouldNotBeFound');
109+
}
110+
111+
public function test_book_markdown_export_direct_pages_are_permission_controlled()
112+
{
113+
$book = $this->entities->bookHasChaptersAndPages();
114+
$page = $book->directPages()->first();
115+
$page->name = 'MyPageWhichShouldNotBeFound';
116+
$page->save();
117+
$this->permissions->disableEntityInheritedPermissions($page);
118+
119+
$resp = $this->asEditor()->get($book->getUrl('/export/markdown'));
120+
121+
$resp->assertSee('# ' . $book->name);
122+
$resp->assertDontSee('MyPageWhichShouldNotBeFound');
123+
}
124+
79125
public function test_book_markdown_export_concats_immediate_pages_with_newlines()
80126
{
81127
/** @var Book $book */

tests/Exports/PdfExportTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function test_page_pdf_export()
1717

1818
$resp = $this->get($page->getUrl('/export/pdf'));
1919
$resp->assertStatus(200);
20-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
20+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.pdf');
2121
}
2222

2323
public function test_book_pdf_export()
@@ -28,7 +28,7 @@ public function test_book_pdf_export()
2828

2929
$resp = $this->get($book->getUrl('/export/pdf'));
3030
$resp->assertStatus(200);
31-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
31+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.pdf');
3232
}
3333

3434
public function test_chapter_pdf_export()
@@ -38,7 +38,7 @@ public function test_chapter_pdf_export()
3838

3939
$resp = $this->get($chapter->getUrl('/export/pdf'));
4040
$resp->assertStatus(200);
41-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
41+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.pdf');
4242
}
4343

4444

tests/Exports/TextExportTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function test_page_text_export()
1414
$resp = $this->get($page->getUrl('/export/plaintext'));
1515
$resp->assertStatus(200);
1616
$resp->assertSee($page->name);
17-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
17+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $page->slug . '.txt');
1818
}
1919

2020
public function test_book_text_export()
@@ -35,7 +35,7 @@ public function test_book_text_export()
3535
$resp->assertSee($directPage->name);
3636
$resp->assertSee('My awesome page');
3737
$resp->assertSee('My little nested page');
38-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
38+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $book->slug . '.txt');
3939
}
4040

4141
public function test_book_text_export_format()
@@ -68,7 +68,7 @@ public function test_chapter_text_export()
6868
$resp->assertSee($chapter->name);
6969
$resp->assertSee($page->name);
7070
$resp->assertSee('This is content within the page!');
71-
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.txt"');
71+
$resp->assertHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . $chapter->slug . '.txt');
7272
}
7373

7474
public function test_chapter_text_export_format()

tests/Uploads/AttachmentTest.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ public function test_file_access_with_open_query_param_provides_inline_response_
324324
$attachmentGet = $this->get($attachment->getUrl(true));
325325
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
326326
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');
327-
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
327+
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'upload_test_file.txt');
328328
$attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
329329

330330
$this->files->deleteAllAttachmentFiles();
@@ -340,7 +340,22 @@ public function test_html_file_access_with_open_forces_plain_content_type()
340340
$attachmentGet = $this->get($attachment->getUrl(true));
341341
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
342342
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=utf-8');
343-
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
343+
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'test_file.html');
344+
345+
$this->files->deleteAllAttachmentFiles();
346+
}
347+
348+
public function test_file_access_name_in_content_disposition_header_is_sanitized()
349+
{
350+
$page = $this->entities->page();
351+
$this->asAdmin();
352+
353+
$attachment = $this->files->uploadAttachmentDataToPage($this, $page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
354+
$attachment->name = "my\\_/super\n_fu\$n_\tfile";
355+
$attachment->save();
356+
357+
$attachmentGet = $this->get($attachment->getUrl(true));
358+
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename*=UTF-8\'\'my_super_fun_file.html');
344359

345360
$this->files->deleteAllAttachmentFiles();
346361
}

0 commit comments

Comments
 (0)