BookStack/app/Uploads/AttachmentService.php
Dan Brown 85154fff69
Added an env configurable file upload size limit
Replaces the old suggestion of setting JS head 'window.uploadLimit'
variable. This new env option will be used by back-end validation and
front-end libs/logic too.

Limits already likely exist within prod environments at a PHP and
webserver level but this allows an app-level limit and centralises the
option on the BookStack side into the .env

Closes #3033
2021-11-14 22:03:22 +00:00

245 lines
7.4 KiB
PHP

<?php
namespace BookStack\Uploads;
use BookStack\Exceptions\FileUploadException;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use League\Flysystem\Util;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
{
protected $fileSystem;
/**
* AttachmentService constructor.
*/
public function __construct(FilesystemManager $fileSystem)
{
$this->fileSystem = $fileSystem;
}
/**
* Get the storage that will be used for storing files.
*/
protected function getStorageDisk(): Storage
{
return $this->fileSystem->disk($this->getStorageDiskName());
}
/**
* Get the name of the storage disk to use.
*/
protected function getStorageDiskName(): string
{
$storageType = config('filesystems.attachments');
// Change to our secure-attachment disk if any of the local options
// are used to prevent escaping that location.
if ($storageType === 'local' || $storageType === 'local_secure') {
$storageType = 'local_secure_attachments';
}
return $storageType;
}
/**
* Change the originally provided path to fit any disk-specific requirements.
* This also ensures the path is kept to the expected root folders.
*/
protected function adjustPathForStorageDisk(string $path): string
{
$path = Util::normalizePath(str_replace('uploads/files/', '', $path));
if ($this->getStorageDiskName() === 'local_secure_attachments') {
return $path;
}
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));
}
/**
* Store a new attachment upon user upload.
*
* @throws FileUploadException
*/
public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($uploadedFile);
$largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $pageId)->max('order');
/** @var Attachment $attachment */
$attachment = Attachment::query()->forceCreate([
'name' => $attachmentName,
'path' => $attachmentPath,
'extension' => $uploadedFile->getClientOriginalExtension(),
'uploaded_to' => $pageId,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1,
]);
return $attachment;
}
/**
* Store an upload, saving to a file and deleting any existing uploads
* attached to that file.
*
* @throws FileUploadException
*/
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment
{
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
}
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($uploadedFile);
$attachment->name = $attachmentName;
$attachment->path = $attachmentPath;
$attachment->external = false;
$attachment->extension = $uploadedFile->getClientOriginalExtension();
$attachment->save();
return $attachment;
}
/**
* Save a new File attachment from a given link and name.
*/
public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
return Attachment::forceCreate([
'name' => $name,
'path' => $link,
'external' => true,
'extension' => '',
'uploaded_to' => $page_id,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1,
]);
}
/**
* Updates the ordering for a listing of attached files.
*/
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
{
foreach ($attachmentOrder as $index => $attachmentId) {
Attachment::query()->where('uploaded_to', '=', $pageId)
->where('id', '=', $attachmentId)
->update(['order' => $index]);
}
}
/**
* Update the details of a file.
*/
public function updateFile(Attachment $attachment, array $requestData): Attachment
{
$attachment->name = $requestData['name'];
$link = trim($requestData['link'] ?? '');
if (!empty($link)) {
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
$attachment->external = true;
$attachment->extension = '';
}
$attachment->path = $requestData['link'];
}
$attachment->save();
return $attachment->refresh();
}
/**
* Delete a File from the database and storage.
*
* @throws Exception
*/
public function deleteFile(Attachment $attachment)
{
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
}
$attachment->delete();
}
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
*/
protected function deleteFileInStorage(Attachment $attachment)
{
$storage = $this->getStorageDisk();
$dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
$storage->delete($this->adjustPathForStorageDisk($attachment->path));
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
}
/**
* Store a file in storage with the given filename.
*
* @throws FileUploadException
*/
protected function putFileInStorage(UploadedFile $uploadedFile): string
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorageDisk();
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
$uploadFileName = Str::random(16) . '-' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
$uploadFileName = Str::random(3) . $uploadFileName;
}
$attachmentPath = $basePath . $uploadFileName;
try {
$storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData);
} catch (Exception $e) {
Log::error('Error when attempting file upload:' . $e->getMessage());
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
}
return $attachmentPath;
}
/**
* Get the file validation rules for attachments.
*/
public function getFileValidationRules(): array
{
return ['file', 'max:' . (config('app.upload_limit') * 1000)];
}
}