mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-05-04 18:08:46 +03:00
Merge branch 'sec_chapter_export' into development
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user