diff --git a/app/Config/filesystems.php b/app/Config/filesystems.php index 95fc35c2a8c..a6b05c8c4aa 100644 --- a/app/Config/filesystems.php +++ b/app/Config/filesystems.php @@ -37,9 +37,14 @@ 'root' => public_path(), ], - 'local_secure' => [ + 'local_secure_attachments' => [ 'driver' => 'local', - 'root' => storage_path(), + 'root' => storage_path('uploads/files/'), + ], + + 'local_secure_images' => [ + 'driver' => 'local', + 'root' => storage_path('uploads/images/'), ], 's3' => [ diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php index b4cb1b88b15..c957e8beafb 100644 --- a/app/Uploads/AttachmentService.php +++ b/app/Uploads/AttachmentService.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; +use League\Flysystem\Util; use Symfony\Component\HttpFoundation\File\UploadedFile; class AttachmentService @@ -27,15 +28,39 @@ public function __construct(FileSystem $fileSystem) * Get the storage that will be used for storing files. */ protected function getStorage(): FileSystemInstance + { + return $this->fileSystem->disk($this->getStorageDiskName()); + } + + /** + * Get the name of the storage disk to use. + */ + protected function getStorageDiskName(): string { $storageType = config('filesystems.attachments'); - // Override default location if set to local public to ensure not visible. - if ($storageType === 'local') { - $storageType = 'local_secure'; + // 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 $this->fileSystem->disk($storageType); + return 'uploads/files/' . $path; } /** @@ -45,26 +70,21 @@ protected function getStorage(): FileSystemInstance */ public function getAttachmentFromStorage(Attachment $attachment): string { - return $this->getStorage()->get($attachment->path); + return $this->getStorage()->get($this->adjustPathForStorageDisk($attachment->path)); } /** * Store a new attachment upon user upload. - * - * @param UploadedFile $uploadedFile - * @param int $page_id - * * @throws FileUploadException - * - * @return Attachment */ - public function saveNewUpload(UploadedFile $uploadedFile, $page_id) + public function saveNewUpload(UploadedFile $uploadedFile, int $page_id): Attachment { $attachmentName = $uploadedFile->getClientOriginalName(); $attachmentPath = $this->putFileInStorage($uploadedFile); - $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order'); + $largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $page_id)->max('order'); - $attachment = Attachment::forceCreate([ + /** @var Attachment $attachment */ + $attachment = Attachment::query()->forceCreate([ 'name' => $attachmentName, 'path' => $attachmentPath, 'extension' => $uploadedFile->getClientOriginalExtension(), @@ -78,17 +98,12 @@ public function saveNewUpload(UploadedFile $uploadedFile, $page_id) } /** - * Store a upload, saving to a file and deleting any existing uploads + * Store an upload, saving to a file and deleting any existing uploads * attached to that file. * - * @param UploadedFile $uploadedFile - * @param Attachment $attachment - * * @throws FileUploadException - * - * @return Attachment */ - public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment) + public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment { if (!$attachment->external) { $this->deleteFileInStorage($attachment); @@ -159,9 +174,6 @@ public function updateFile(Attachment $attachment, array $requestData): Attachme /** * Delete a File from the database and storage. - * - * @param Attachment $attachment - * * @throws Exception */ public function deleteFile(Attachment $attachment) @@ -179,15 +191,13 @@ public function deleteFile(Attachment $attachment) /** * Delete a file from the filesystem it sits on. * Cleans any empty leftover folders. - * - * @param Attachment $attachment */ protected function deleteFileInStorage(Attachment $attachment) { $storage = $this->getStorage(); - $dirPath = dirname($attachment->path); + $dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path)); - $storage->delete($attachment->path); + $storage->delete($this->adjustPathForStorageDisk($attachment->path)); if (count($storage->allFiles($dirPath)) === 0) { $storage->deleteDirectory($dirPath); } @@ -195,14 +205,9 @@ protected function deleteFileInStorage(Attachment $attachment) /** * Store a file in storage with the given filename. - * - * @param UploadedFile $uploadedFile - * * @throws FileUploadException - * - * @return string */ - protected function putFileInStorage(UploadedFile $uploadedFile) + protected function putFileInStorage(UploadedFile $uploadedFile): string { $attachmentData = file_get_contents($uploadedFile->getRealPath()); @@ -210,14 +215,14 @@ protected function putFileInStorage(UploadedFile $uploadedFile) $basePath = 'uploads/files/' . date('Y-m-M') . '/'; $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension(); - while ($storage->exists($basePath . $uploadFileName)) { + while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) { $uploadFileName = Str::random(3) . $uploadFileName; } $attachmentPath = $basePath . $uploadFileName; try { - $storage->put($attachmentPath, $attachmentData); + $storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData); } catch (Exception $e) { Log::error('Error when attempting file upload:' . $e->getMessage()); diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 2c38c24f4f7..eff8005d716 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -14,6 +14,7 @@ use Illuminate\Support\Str; use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\ImageManager; +use League\Flysystem\Util; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageService @@ -38,16 +39,43 @@ public function __construct(Image $image, ImageManager $imageTool, FileSystem $f /** * Get the storage that will be used for storing images. */ - protected function getStorage(string $type = ''): FileSystemInstance + protected function getStorage(string $imageType = ''): FileSystemInstance + { + return $this->fileSystem->disk($this->getStorageDiskName($imageType)); + } + + /** + * 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 $imageType = ''): string + { + $path = Util::normalizePath(str_replace('uploads/images/', '', $path)); + + if ($this->getStorageDiskName($imageType) === 'local_secure_images') { + return $path; + } + + return 'uploads/images/' . $path; + } + + /** + * Get the name of the storage disk to use. + */ + protected function getStorageDiskName(string $imageType): string { $storageType = config('filesystems.images'); // Ensure system images (App logo) are uploaded to a public space - if ($type === 'system' && $storageType === 'local_secure') { + if ($imageType === 'system' && $storageType === 'local_secure') { $storageType = 'local'; } - return $this->fileSystem->disk($storageType); + if ($storageType === 'local_secure') { + $storageType = 'local_secure_images'; + } + + return $storageType; } /** @@ -104,7 +132,7 @@ public function saveNew(string $imageName, string $imageData, string $type, int $imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/'; - while ($storage->exists($imagePath . $fileName)) { + while ($storage->exists($this->adjustPathForStorageDisk($imagePath . $fileName, $type))) { $fileName = Str::random(3) . $fileName; } @@ -114,7 +142,7 @@ public function saveNew(string $imageName, string $imageData, string $type, int } try { - $this->saveImageDataInPublicSpace($storage, $fullPath, $imageData); + $this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($fullPath, $type), $imageData); } catch (Exception $e) { \Log::error('Error when attempting image upload:' . $e->getMessage()); @@ -216,13 +244,13 @@ public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRat } $storage = $this->getStorage($image->type); - if ($storage->exists($thumbFilePath)) { + if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) { return $this->getPublicUrl($thumbFilePath); } - $thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio); + $thumbData = $this->resizeImage($storage->get($this->adjustPathForStorageDisk($imagePath, $image->type)), $width, $height, $keepRatio); - $this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData); + $this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($thumbFilePath, $image->type), $thumbData); $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72); return $this->getPublicUrl($thumbFilePath); @@ -279,10 +307,8 @@ protected function resizeImage(string $imageData, $width = 220, $height = null, */ public function getImageData(Image $image): string { - $imagePath = $image->path; $storage = $this->getStorage(); - - return $storage->get($imagePath); + return $storage->get($this->adjustPathForStorageDisk($image->path, $image->type)); } /** @@ -292,7 +318,7 @@ public function getImageData(Image $image): string */ public function destroy(Image $image) { - $this->destroyImagesFromPath($image->path); + $this->destroyImagesFromPath($image->path, $image->type); $image->delete(); } @@ -300,9 +326,10 @@ public function destroy(Image $image) * Destroys an image at the given path. * Searches for image thumbnails in addition to main provided path. */ - protected function destroyImagesFromPath(string $path): bool + protected function destroyImagesFromPath(string $path, string $imageType): bool { - $storage = $this->getStorage(); + $path = $this->adjustPathForStorageDisk($path, $imageType); + $storage = $this->getStorage($imageType); $imageFolder = dirname($path); $imageFileName = basename($path); @@ -326,7 +353,7 @@ protected function destroyImagesFromPath(string $path): bool } /** - * Check whether or not a folder is empty. + * Check whether a folder is empty. */ protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool { @@ -374,7 +401,7 @@ public function deleteUnusedImages(bool $checkRevisions = true, bool $dryRun = t } /** - * Convert a image URI to a Base64 encoded string. + * Convert an image URI to a Base64 encoded string. * Attempts to convert the URL to a system storage url then * fetch the data from the disk or storage location. * Returns null if the image data cannot be fetched from storage. @@ -388,6 +415,7 @@ public function imageUriToBase64(string $uri): ?string return null; } + $storagePath = $this->adjustPathForStorageDisk($storagePath); $storage = $this->getStorage(); $imageData = null; if ($storage->exists($storagePath)) {