]> BookStack Code Mirror - bookstack/commitdiff
Extracted download response logic to its own class
authorDan Brown <redacted>
Wed, 8 Jun 2022 22:50:42 +0000 (23:50 +0100)
committerDan Brown <redacted>
Wed, 8 Jun 2022 22:50:42 +0000 (23:50 +0100)
Cleans up base controller and groups up download & streaming logic for
potential future easier addition of range request support.

app/Http/Controllers/Api/BookExportApiController.php
app/Http/Controllers/Api/ChapterExportApiController.php
app/Http/Controllers/Api/PageExportApiController.php
app/Http/Controllers/AttachmentController.php
app/Http/Controllers/BookExportController.php
app/Http/Controllers/ChapterExportController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/PageExportController.php
app/Http/Responses/DownloadResponseFactory.php [new file with mode: 0644]
app/Uploads/AttachmentService.php
app/Util/WebSafeMimeSniffer.php

index 028bc3a817ebf726b358ca0f7b8d47f393695bf8..84090befba53249284e267bf77d767d88888cecb 100644 (file)
@@ -26,7 +26,7 @@ class BookExportApiController extends ApiController
         $book = Book::visible()->findOrFail($id);
         $pdfContent = $this->exportFormatter->bookToPdf($book);
 
-        return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
+        return $this->download()->directly($pdfContent, $book->slug . '.pdf');
     }
 
     /**
@@ -39,7 +39,7 @@ class BookExportApiController extends ApiController
         $book = Book::visible()->findOrFail($id);
         $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
 
-        return $this->downloadResponse($htmlContent, $book->slug . '.html');
+        return $this->download()->directly($htmlContent, $book->slug . '.html');
     }
 
     /**
@@ -50,7 +50,7 @@ class BookExportApiController extends ApiController
         $book = Book::visible()->findOrFail($id);
         $textContent = $this->exportFormatter->bookToPlainText($book);
 
-        return $this->downloadResponse($textContent, $book->slug . '.txt');
+        return $this->download()->directly($textContent, $book->slug . '.txt');
     }
 
     /**
@@ -61,6 +61,6 @@ class BookExportApiController extends ApiController
         $book = Book::visible()->findOrFail($id);
         $markdown = $this->exportFormatter->bookToMarkdown($book);
 
-        return $this->downloadResponse($markdown, $book->slug . '.md');
+        return $this->download()->directly($markdown, $book->slug . '.md');
     }
 }
index 5715ab2e37c6c9f7e682ad69b407f27153e3934e..faf5d812e207c667170fd8c361f4a976ee3d7f11 100644 (file)
@@ -29,7 +29,7 @@ class ChapterExportApiController extends ApiController
         $chapter = Chapter::visible()->findOrFail($id);
         $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
 
-        return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
+        return $this->download()->directly($pdfContent, $chapter->slug . '.pdf');
     }
 
     /**
@@ -42,7 +42,7 @@ class ChapterExportApiController extends ApiController
         $chapter = Chapter::visible()->findOrFail($id);
         $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
 
-        return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
+        return $this->download()->directly($htmlContent, $chapter->slug . '.html');
     }
 
     /**
@@ -53,7 +53,7 @@ class ChapterExportApiController extends ApiController
         $chapter = Chapter::visible()->findOrFail($id);
         $textContent = $this->exportFormatter->chapterToPlainText($chapter);
 
-        return $this->downloadResponse($textContent, $chapter->slug . '.txt');
+        return $this->download()->directly($textContent, $chapter->slug . '.txt');
     }
 
     /**
@@ -64,6 +64,6 @@ class ChapterExportApiController extends ApiController
         $chapter = Chapter::visible()->findOrFail($id);
         $markdown = $this->exportFormatter->chapterToMarkdown($chapter);
 
-        return $this->downloadResponse($markdown, $chapter->slug . '.md');
+        return $this->download()->directly($markdown, $chapter->slug . '.md');
     }
 }
index ce5700c79b9add01a9b2eb8d418f5b3a785344e8..f2edffba3d8b8934175705ada9a4327d2d0724b4 100644 (file)
@@ -26,7 +26,7 @@ class PageExportApiController extends ApiController
         $page = Page::visible()->findOrFail($id);
         $pdfContent = $this->exportFormatter->pageToPdf($page);
 
-        return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
+        return $this->download()->directly($pdfContent, $page->slug . '.pdf');
     }
 
     /**
@@ -39,7 +39,7 @@ class PageExportApiController extends ApiController
         $page = Page::visible()->findOrFail($id);
         $htmlContent = $this->exportFormatter->pageToContainedHtml($page);
 
-        return $this->downloadResponse($htmlContent, $page->slug . '.html');
+        return $this->download()->directly($htmlContent, $page->slug . '.html');
     }
 
     /**
@@ -50,7 +50,7 @@ class PageExportApiController extends ApiController
         $page = Page::visible()->findOrFail($id);
         $textContent = $this->exportFormatter->pageToPlainText($page);
 
-        return $this->downloadResponse($textContent, $page->slug . '.txt');
+        return $this->download()->directly($textContent, $page->slug . '.txt');
     }
 
     /**
@@ -61,6 +61,6 @@ class PageExportApiController extends ApiController
         $page = Page::visible()->findOrFail($id);
         $markdown = $this->exportFormatter->pageToMarkdown($page);
 
-        return $this->downloadResponse($markdown, $page->slug . '.md');
+        return $this->download()->directly($markdown, $page->slug . '.md');
     }
 }
index 0a092b63ae11385e4d67dae6218109b2e2f659fa..03e362f4a78f273573195d120ae806a9d65931ae 100644 (file)
@@ -233,10 +233,10 @@ class AttachmentController extends Controller
         $attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment);
 
         if ($request->get('open') === 'true') {
-            return $this->streamedInlineDownloadResponse($attachmentStream, $fileName);
+            return $this->download()->streamedInline($attachmentStream, $fileName);
         }
 
-        return $this->streamedDownloadResponse($attachmentStream, $fileName);
+        return $this->download()->streamedDirectly($attachmentStream, $fileName);
     }
 
     /**
index 7f6dd801752b5e3901cb6188e3dc200888f32984..cc8d48a35511e5c6504c69b33512c9164fd22793 100644 (file)
@@ -31,7 +31,7 @@ class BookExportController extends Controller
         $book = $this->bookRepo->getBySlug($bookSlug);
         $pdfContent = $this->exportFormatter->bookToPdf($book);
 
-        return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
+        return $this->download()->directly($pdfContent, $bookSlug . '.pdf');
     }
 
     /**
@@ -44,7 +44,7 @@ class BookExportController extends Controller
         $book = $this->bookRepo->getBySlug($bookSlug);
         $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
 
-        return $this->downloadResponse($htmlContent, $bookSlug . '.html');
+        return $this->download()->directly($htmlContent, $bookSlug . '.html');
     }
 
     /**
@@ -55,7 +55,7 @@ class BookExportController extends Controller
         $book = $this->bookRepo->getBySlug($bookSlug);
         $textContent = $this->exportFormatter->bookToPlainText($book);
 
-        return $this->downloadResponse($textContent, $bookSlug . '.txt');
+        return $this->download()->directly($textContent, $bookSlug . '.txt');
     }
 
     /**
@@ -66,6 +66,6 @@ class BookExportController extends Controller
         $book = $this->bookRepo->getBySlug($bookSlug);
         $textContent = $this->exportFormatter->bookToMarkdown($book);
 
-        return $this->downloadResponse($textContent, $bookSlug . '.md');
+        return $this->download()->directly($textContent, $bookSlug . '.md');
     }
 }
index 480280c99ef6dc83a169ba1762d5e4fc0edd4d36..fd56d91b3359218b6f1bf0218761132200bb803d 100644 (file)
@@ -33,7 +33,7 @@ class ChapterExportController extends Controller
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
 
-        return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
+        return $this->download()->directly($pdfContent, $chapterSlug . '.pdf');
     }
 
     /**
@@ -47,7 +47,7 @@ class ChapterExportController extends Controller
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
 
-        return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
+        return $this->download()->directly($containedHtml, $chapterSlug . '.html');
     }
 
     /**
@@ -60,7 +60,7 @@ class ChapterExportController extends Controller
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $chapterText = $this->exportFormatter->chapterToPlainText($chapter);
 
-        return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+        return $this->download()->directly($chapterText, $chapterSlug . '.txt');
     }
 
     /**
@@ -70,10 +70,9 @@ class ChapterExportController extends Controller
      */
     public function markdown(string $bookSlug, string $chapterSlug)
     {
-        // TODO: This should probably export to a zip file.
         $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
         $chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
 
-        return $this->downloadResponse($chapterText, $chapterSlug . '.md');
+        return $this->download()->directly($chapterText, $chapterSlug . '.md');
     }
 }
index 5b2221fc1235c412ae00c57971d77425ec8b737c..f6dc1dbca43cb9b46ce60ef9d48105f16a04c575 100644 (file)
@@ -4,15 +4,13 @@ namespace BookStack\Http\Controllers;
 
 use BookStack\Exceptions\NotifyException;
 use BookStack\Facades\Activity;
+use BookStack\Http\Responses\DownloadResponseFactory;
 use BookStack\Interfaces\Loggable;
 use BookStack\Model;
-use BookStack\Util\WebSafeMimeSniffer;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Http\Response;
 use Illuminate\Routing\Controller as BaseController;
-use Symfony\Component\HttpFoundation\StreamedResponse;
 
 abstract class Controller extends BaseController
 {
@@ -110,74 +108,11 @@ abstract class Controller extends BaseController
     }
 
     /**
-     * Create a response that forces a download in the browser.
+     * Create and return a new download response factory using the current request.
      */
-    protected function downloadResponse(string $content, string $fileName): Response
+    protected function download(): DownloadResponseFactory
     {
-        return response()->make($content, 200, [
-            'Content-Type'           => 'application/octet-stream',
-            'Content-Disposition'    => 'attachment; filename="' . str_replace('"', '', $fileName) . '"',
-            'X-Content-Type-Options' => 'nosniff',
-        ]);
-    }
-
-    /**
-     * Create a response that forces a download, from a given stream of content.
-     */
-    protected function streamedDownloadResponse($stream, string $fileName): StreamedResponse
-    {
-        return response()->stream(function () use ($stream) {
-
-            // End & flush the output buffer, if we're in one, otherwise we still use memory.
-            // Output buffer may or may not exist depending on PHP `output_buffering` setting.
-            // Ignore in testing since output buffers are used to gather a response.
-            if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
-                ob_end_clean();
-            }
-
-            fpassthru($stream);
-            fclose($stream);
-        }, 200, [
-            'Content-Type'           => 'application/octet-stream',
-            'Content-Disposition'    => 'attachment; filename="' . str_replace('"', '', $fileName) . '"',
-            'X-Content-Type-Options' => 'nosniff',
-        ]);
-    }
-
-    /**
-     * Create a file download response that provides the file with a content-type
-     * correct for the file, in a way so the browser can show the content in browser.
-     */
-    protected function inlineDownloadResponse(string $content, string $fileName): Response
-    {
-        $mime = (new WebSafeMimeSniffer())->sniff($content);
-
-        return response()->make($content, 200, [
-            'Content-Type'           => $mime,
-            'Content-Disposition'    => 'inline; filename="' . str_replace('"', '', $fileName) . '"',
-            'X-Content-Type-Options' => 'nosniff',
-        ]);
-    }
-
-    /**
-     * Create a file download response that provides the file with a content-type
-     * correct for the file, in a way so the browser can show the content in browser,
-     * for a given content stream.
-     */
-    protected function streamedInlineDownloadResponse($stream, string $fileName): StreamedResponse
-    {
-        $sniffContent = fread($stream, 1000);
-        $mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
-
-        return response()->stream(function () use ($sniffContent, $stream) {
-            echo $sniffContent;
-            fpassthru($stream);
-            fclose($stream);
-        }, 200, [
-            'Content-Type'           => $mime,
-            'Content-Disposition'    => 'inline; filename="' . str_replace('"', '', $fileName) . '"',
-            'X-Content-Type-Options' => 'nosniff',
-        ]);
+        return new DownloadResponseFactory(request());
     }
 
     /**
index 0287916de28f40008eeb2d16e948d6274eb77a3a..62101d3390fccca880259f76d824cde7deb2396c 100644 (file)
@@ -36,7 +36,7 @@ class PageExportController extends Controller
         $page->html = (new PageContent($page))->render();
         $pdfContent = $this->exportFormatter->pageToPdf($page);
 
-        return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
+        return $this->download()->directly($pdfContent, $pageSlug . '.pdf');
     }
 
     /**
@@ -51,7 +51,7 @@ class PageExportController extends Controller
         $page->html = (new PageContent($page))->render();
         $containedHtml = $this->exportFormatter->pageToContainedHtml($page);
 
-        return $this->downloadResponse($containedHtml, $pageSlug . '.html');
+        return $this->download()->directly($containedHtml, $pageSlug . '.html');
     }
 
     /**
@@ -64,7 +64,7 @@ class PageExportController extends Controller
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $pageText = $this->exportFormatter->pageToPlainText($page);
 
-        return $this->downloadResponse($pageText, $pageSlug . '.txt');
+        return $this->download()->directly($pageText, $pageSlug . '.txt');
     }
 
     /**
@@ -77,6 +77,6 @@ class PageExportController extends Controller
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
         $pageText = $this->exportFormatter->pageToMarkdown($page);
 
-        return $this->downloadResponse($pageText, $pageSlug . '.md');
+        return $this->download()->directly($pageText, $pageSlug . '.md');
     }
 }
diff --git a/app/Http/Responses/DownloadResponseFactory.php b/app/Http/Responses/DownloadResponseFactory.php
new file mode 100644 (file)
index 0000000..cdf8e31
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace BookStack\Http\Responses;
+
+use BookStack\Util\WebSafeMimeSniffer;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Symfony\Component\HttpFoundation\StreamedResponse;
+
+class DownloadResponseFactory
+{
+    protected Request $request;
+
+    public function __construct(Request $request)
+    {
+        $this->request = $request;
+    }
+
+    /**
+     * Create a response that directly forces a download in the browser.
+     */
+    public function directly(string $content, string $fileName): Response
+    {
+        return response()->make($content, 200, $this->getHeaders($fileName));
+    }
+
+    /**
+     * Create a response that forces a download, from a given stream of content.
+     */
+    public function streamedDirectly($stream, string $fileName): StreamedResponse
+    {
+        return response()->stream(function () use ($stream) {
+
+            // End & flush the output buffer, if we're in one, otherwise we still use memory.
+            // Output buffer may or may not exist depending on PHP `output_buffering` setting.
+            // Ignore in testing since output buffers are used to gather a response.
+            if (!empty(ob_get_status()) && !app()->runningUnitTests()) {
+                ob_end_clean();
+            }
+
+            fpassthru($stream);
+            fclose($stream);
+        }, 200, $this->getHeaders($fileName));
+    }
+
+    /**
+     * Create a file download response that provides the file with a content-type
+     * correct for the file, in a way so the browser can show the content in browser,
+     * for a given content stream.
+     */
+    public function streamedInline($stream, string $fileName): StreamedResponse
+    {
+        $sniffContent = fread($stream, 2000);
+        $mime = (new WebSafeMimeSniffer())->sniff($sniffContent);
+
+        return response()->stream(function () use ($sniffContent, $stream) {
+            echo $sniffContent;
+            fpassthru($stream);
+            fclose($stream);
+        }, 200, $this->getHeaders($fileName, $mime));
+    }
+
+    /**
+     * Get the common headers to provide for a download response.
+     */
+    protected function getHeaders(string $fileName, string $mime = 'application/octet-stream'): array
+    {
+        $disposition = ($mime === 'application/octet-stream') ? 'attachment' : 'inline';
+        $downloadName = str_replace('"', '', $fileName);
+
+        return [
+            'Content-Type'           => $mime,
+            'Content-Disposition'    => "{$disposition}; filename=\"{$downloadName}\"",
+            'X-Content-Type-Options' => 'nosniff',
+        ];
+    }
+}
\ No newline at end of file
index 9d1f96ae42f99178f6b3fd10406a4572805b5943..6a92cb5a543c6a24b6ae069679965b09cb5dbfbc 100644 (file)
@@ -63,16 +63,6 @@ class AttachmentService
         return 'uploads/files/' . $path;
     }
 
-    /**
-     * Get an attachment from storage.
-     *
-     * @throws FileNotFoundException
-     */
-    public function getAttachmentFromStorage(Attachment $attachment): string
-    {
-        return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
-    }
-
     /**
      * Stream an attachment from storage.
      *
index 6861add724dbf2bf1ca6a0a34bd8ae4109b66ded..b182d8ac19b39ef6eec4e09acdbe00d8d86c4854 100644 (file)
@@ -24,6 +24,7 @@ class WebSafeMimeSniffer
         'audio/opus',
         'audio/wav',
         'audio/webm',
+        'audio/x-m4a',
         'image/apng',
         'image/bmp',
         'image/jpeg',
Morty Proxy This is a proxified and sanitized view of the page, visit original site.