Added back-end attachments-in-browser support

A query string will cause attachments to be provided inline
with an appropriate mime type.
Remaining actions:
- Tests
- Front-end functionality
- Config option?
This commit is contained in:
Dan Brown 2021-06-06 00:51:06 +01:00
parent a8471b2c66
commit 888f435651
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 34 additions and 14 deletions

View File

@ -14,16 +14,14 @@ use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller class AttachmentController extends Controller
{ {
protected $attachmentService; protected $attachmentService;
protected $attachment;
protected $pageRepo; protected $pageRepo;
/** /**
* AttachmentController constructor. * AttachmentController constructor.
*/ */
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo) public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
{ {
$this->attachmentService = $attachmentService; $this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->pageRepo = $pageRepo; $this->pageRepo = $pageRepo;
} }
@ -67,7 +65,7 @@ class AttachmentController extends Controller
'file' => 'required|file' 'file' => 'required|file'
]); ]);
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId); $attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('view', $attachment->page); $this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page); $this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment); $this->checkOwnablePermission('attachment-create', $attachment);
@ -89,7 +87,7 @@ class AttachmentController extends Controller
*/ */
public function getUpdateForm(string $attachmentId) public function getUpdateForm(string $attachmentId)
{ {
$attachment = $this->attachment->findOrFail($attachmentId); $attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $attachment->page); $this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment); $this->checkOwnablePermission('attachment-create', $attachment);
@ -202,9 +200,10 @@ class AttachmentController extends Controller
* @throws FileNotFoundException * @throws FileNotFoundException
* @throws NotFoundException * @throws NotFoundException
*/ */
public function get(string $attachmentId) public function get(Request $request, string $attachmentId)
{ {
$attachment = $this->attachment->findOrFail($attachmentId); /** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
try { try {
$page = $this->pageRepo->getById($attachment->uploaded_to); $page = $this->pageRepo->getById($attachment->uploaded_to);
} catch (NotFoundException $exception) { } catch (NotFoundException $exception) {
@ -217,8 +216,13 @@ class AttachmentController extends Controller
return redirect($attachment->path); return redirect($attachment->path);
} }
$fileName = $attachment->getFileName();
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment); $attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
if ($request->get('open') === 'true') {
return $this->inlineDownloadResponse($attachmentContents, $fileName);
}
return $this->downloadResponse($attachmentContents, $fileName);
} }
/** /**
@ -227,7 +231,7 @@ class AttachmentController extends Controller
*/ */
public function delete(string $attachmentId) public function delete(string $attachmentId)
{ {
$attachment = $this->attachment->findOrFail($attachmentId); $attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment); $this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment); $this->attachmentService->deleteFile($attachment);
return response()->json(['message' => trans('entities.attachments_deleted')]); return response()->json(['message' => trans('entities.attachments_deleted')]);

View File

@ -6,6 +6,7 @@ use BookStack\Facades\Activity;
use BookStack\Interfaces\Loggable; use BookStack\Interfaces\Loggable;
use BookStack\HasCreatorAndUpdater; use BookStack\HasCreatorAndUpdater;
use BookStack\Model; use BookStack\Model;
use finfo;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\Exceptions\HttpResponseException;
@ -121,6 +122,20 @@ abstract class Controller extends BaseController
]); ]);
} }
/**
* 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
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
return response()->make($content, 200, [
'Content-Type' => $mime,
'Content-Disposition' => 'inline; filename="' . $fileName . '"'
]);
}
/** /**
* Show a positive, successful notification to the user on next view load. * Show a positive, successful notification to the user on next view load.
*/ */

View File

@ -3,8 +3,10 @@
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use Exception; use Exception;
use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Log;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService class AttachmentService
@ -38,11 +40,9 @@ class AttachmentService
/** /**
* Get an attachment from storage. * Get an attachment from storage.
* @param Attachment $attachment * @throws FileNotFoundException
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/ */
public function getAttachmentFromStorage(Attachment $attachment) public function getAttachmentFromStorage(Attachment $attachment): string
{ {
return $this->getStorage()->get($attachment->path); return $this->getStorage()->get($attachment->path);
} }
@ -202,7 +202,7 @@ class AttachmentService
try { try {
$storage->put($attachmentPath, $attachmentData); $storage->put($attachmentPath, $attachmentData);
} catch (Exception $e) { } catch (Exception $e) {
\Log::error('Error when attempting file upload:' . $e->getMessage()); Log::error('Error when attempting file upload:' . $e->getMessage());
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath])); throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
} }

View File

@ -8,6 +8,7 @@
"php": "^7.3|^8.0", "php": "^7.3|^8.0",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",