mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-02 23:40:04 +00:00
Started work on local_secure_restricted image option
This commit is contained in:
parent
9da3130a12
commit
27ac122502
3 changed files with 79 additions and 21 deletions
app
|
@ -33,7 +33,7 @@ class ImageController extends Controller
|
||||||
*/
|
*/
|
||||||
public function showImage(string $path)
|
public function showImage(string $path)
|
||||||
{
|
{
|
||||||
if (!$this->imageService->pathExistsInLocalSecure($path)) {
|
if (!$this->imageService->pathAccessibleInLocalSecure($path)) {
|
||||||
throw (new NotFoundException(trans('errors.image_not_found')))
|
throw (new NotFoundException(trans('errors.image_not_found')))
|
||||||
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
||||||
->setDetails(trans('errors.image_not_found_details'));
|
->setDetails(trans('errors.image_not_found_details'));
|
||||||
|
|
|
@ -41,7 +41,7 @@ class AttachmentService
|
||||||
|
|
||||||
// Change to our secure-attachment disk if any of the local options
|
// Change to our secure-attachment disk if any of the local options
|
||||||
// are used to prevent escaping that location.
|
// are used to prevent escaping that location.
|
||||||
if ($storageType === 'local' || $storageType === 'local_secure') {
|
if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_with_permissions') {
|
||||||
$storageType = 'local_secure_attachments';
|
$storageType = 'local_secure_attachments';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
namespace BookStack\Uploads;
|
namespace BookStack\Uploads;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use ErrorException;
|
use ErrorException;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
@ -24,20 +27,15 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class ImageService
|
class ImageService
|
||||||
{
|
{
|
||||||
protected $imageTool;
|
protected ImageManager $imageTool;
|
||||||
protected $cache;
|
protected Cache $cache;
|
||||||
protected $storageUrl;
|
protected $storageUrl;
|
||||||
protected $image;
|
protected FilesystemManager $fileSystem;
|
||||||
protected $fileSystem;
|
|
||||||
|
|
||||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
|
||||||
/**
|
public function __construct(ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
|
||||||
* ImageService constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(Image $image, ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
|
|
||||||
{
|
{
|
||||||
$this->image = $image;
|
|
||||||
$this->imageTool = $imageTool;
|
$this->imageTool = $imageTool;
|
||||||
$this->fileSystem = $fileSystem;
|
$this->fileSystem = $fileSystem;
|
||||||
$this->cache = $cache;
|
$this->cache = $cache;
|
||||||
|
@ -55,9 +53,18 @@ class ImageService
|
||||||
* Check if local secure image storage (Fetched behind authentication)
|
* Check if local secure image storage (Fetched behind authentication)
|
||||||
* is currently active in the instance.
|
* is currently active in the instance.
|
||||||
*/
|
*/
|
||||||
protected function usingSecureImages(): bool
|
protected function usingSecureImages(string $imageType = 'gallery'): bool
|
||||||
{
|
{
|
||||||
return $this->getStorageDiskName('gallery') === 'local_secure_images';
|
return $this->getStorageDiskName($imageType) === 'local_secure_images';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if "local secure restricted" (Fetched behind auth, with permissions enforced)
|
||||||
|
* is currently active in the instance.
|
||||||
|
*/
|
||||||
|
protected function usingSecureRestrictedImages()
|
||||||
|
{
|
||||||
|
return config('filesystems.images') === 'local_secure_restricted';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,7 +75,7 @@ class ImageService
|
||||||
{
|
{
|
||||||
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
|
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
|
||||||
|
|
||||||
if ($this->getStorageDiskName($imageType) === 'local_secure_images') {
|
if ($this->usingSecureImages($imageType)) {
|
||||||
return $path;
|
return $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +94,9 @@ class ImageService
|
||||||
$storageType = 'local';
|
$storageType = 'local';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($storageType === 'local_secure') {
|
// Rename local_secure options to get our image specific storage driver which
|
||||||
|
// is scoped to the relevant image directories.
|
||||||
|
if ($storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
|
||||||
$storageType = 'local_secure_images';
|
$storageType = 'local_secure_images';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,8 +188,8 @@ class ImageService
|
||||||
$imageDetails['updated_by'] = $userId;
|
$imageDetails['updated_by'] = $userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$image = $this->image->newInstance();
|
$image = (new Image())->forceFill($imageDetails);
|
||||||
$image->forceFill($imageDetails)->save();
|
$image->save();
|
||||||
|
|
||||||
return $image;
|
return $image;
|
||||||
}
|
}
|
||||||
|
@ -451,7 +460,7 @@ class ImageService
|
||||||
$types = ['gallery', 'drawio'];
|
$types = ['gallery', 'drawio'];
|
||||||
$deletedPaths = [];
|
$deletedPaths = [];
|
||||||
|
|
||||||
$this->image->newQuery()->whereIn('type', $types)
|
Image::query()->whereIn('type', $types)
|
||||||
->chunk(1000, function ($images) use ($checkRevisions, &$deletedPaths, $dryRun) {
|
->chunk(1000, function ($images) use ($checkRevisions, &$deletedPaths, $dryRun) {
|
||||||
foreach ($images as $image) {
|
foreach ($images as $image) {
|
||||||
$searchQuery = '%' . basename($image->path) . '%';
|
$searchQuery = '%' . basename($image->path) . '%';
|
||||||
|
@ -511,14 +520,19 @@ class ImageService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given path exists in the local secure image system.
|
* Check if the given path exists and is accessible in the local secure image system.
|
||||||
* Returns false if local_secure is not in use.
|
* Returns false if local_secure is not in use, if the file does not exist, if the
|
||||||
|
* file is likely not a valid image, or if permission does not allow access.
|
||||||
*/
|
*/
|
||||||
public function pathExistsInLocalSecure(string $imagePath): bool
|
public function pathAccessibleInLocalSecure(string $imagePath): bool
|
||||||
{
|
{
|
||||||
/** @var FilesystemAdapter $disk */
|
/** @var FilesystemAdapter $disk */
|
||||||
$disk = $this->getStorageDisk('gallery');
|
$disk = $this->getStorageDisk('gallery');
|
||||||
|
|
||||||
|
if ($this->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check local_secure is active
|
// Check local_secure is active
|
||||||
return $this->usingSecureImages()
|
return $this->usingSecureImages()
|
||||||
&& $disk instanceof FilesystemAdapter
|
&& $disk instanceof FilesystemAdapter
|
||||||
|
@ -528,6 +542,50 @@ class ImageService
|
||||||
&& strpos($disk->getMimetype($imagePath), 'image/') === 0;
|
&& strpos($disk->getMimetype($imagePath), 'image/') === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the current user has access to the relation
|
||||||
|
* of the image at the given path.
|
||||||
|
*/
|
||||||
|
protected function checkUserHasAccessToRelationOfImageAtPath(string $path): bool
|
||||||
|
{
|
||||||
|
// Strip thumbnail element from path if existing
|
||||||
|
$originalPathSplit = array_filter(explode('/', $path), function(string $part) {
|
||||||
|
$resizedDir = (strpos($part, 'thumbs-') === 0 || strpos($part, 'scaled-') === 0);
|
||||||
|
$missingExtension = strpos($part, '.') === false;
|
||||||
|
return !($resizedDir && $missingExtension);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build a database-format image path and search for the image entry
|
||||||
|
$fullPath = '/uploads/images/' . ltrim(implode('/', $originalPathSplit), '/');
|
||||||
|
$image = Image::query()->where('path', '=', $fullPath)->first();
|
||||||
|
|
||||||
|
if (is_null($image)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageType = $image->type;
|
||||||
|
|
||||||
|
// Allow user or system (logo) images
|
||||||
|
// (No specific relation control but may still have access controlled by auth)
|
||||||
|
if ($imageType === 'user' || $imageType === 'system') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($imageType === 'gallery' || $imageType === 'drawio') {
|
||||||
|
return Page::visible()->where('id', '=', $image->uploaded_to)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($imageType === 'cover_book') {
|
||||||
|
return Book::visible()->where('id', '=', $image->uploaded_to)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($imageType === 'cover_bookshelf') {
|
||||||
|
return Bookshelf::visible()->where('id', '=', $image->uploaded_to)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For the given path, if existing, provide a response that will stream the image contents.
|
* For the given path, if existing, provide a response that will stream the image contents.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue