mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-11-23 17:22:23 +03:00
Exports: Updated perm checking for images in ZIP exports
For #5885 Adds to, uses and cleans-up central permission checking in ImageService to mirror that which would be experienced by users in the UI to result in the same image access conditions. Adds testing to cover.
This commit is contained in:
@@ -15,6 +15,7 @@ use BookStack\Exports\ZipExports\Models\ZipExportPage;
|
|||||||
use BookStack\Permissions\Permission;
|
use BookStack\Permissions\Permission;
|
||||||
use BookStack\Uploads\Attachment;
|
use BookStack\Uploads\Attachment;
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
|
use BookStack\Uploads\ImageService;
|
||||||
|
|
||||||
class ZipExportReferences
|
class ZipExportReferences
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,7 @@ class ZipExportReferences
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected ZipReferenceParser $parser,
|
protected ZipReferenceParser $parser,
|
||||||
|
protected ImageService $imageService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,10 +135,17 @@ class ZipExportReferences
|
|||||||
return "[[bsexport:image:{$model->id}]]";
|
return "[[bsexport:image:{$model->id}]]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find and include images if in visibility
|
// Get the page which we'll reference this image upon
|
||||||
$page = $model->getPage();
|
$page = $model->getPage();
|
||||||
$pageExportModel = $this->pages[$page->id] ?? ($exportModel instanceof ZipExportPage ? $exportModel : null);
|
$pageExportModel = null;
|
||||||
if (isset($this->images[$model->id]) || ($page && $pageExportModel && userCan(Permission::PageView, $page))) {
|
if ($page && isset($this->pages[$page->id])) {
|
||||||
|
$pageExportModel = $this->pages[$page->id];
|
||||||
|
} elseif ($exportModel instanceof ZipExportPage) {
|
||||||
|
$pageExportModel = $exportModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the image to the export if it's accessible or just return the existing reference if already added
|
||||||
|
if (isset($this->images[$model->id]) || ($pageExportModel && $this->imageService->imageAccessible($model))) {
|
||||||
if (!isset($this->images[$model->id])) {
|
if (!isset($this->images[$model->id])) {
|
||||||
$exportImage = ZipExportImage::fromModel($model, $files);
|
$exportImage = ZipExportImage::fromModel($model, $files);
|
||||||
$this->images[$model->id] = $exportImage;
|
$this->images[$model->id] = $exportImage;
|
||||||
@@ -144,6 +153,7 @@ class ZipExportReferences
|
|||||||
}
|
}
|
||||||
return "[[bsexport:image:{$model->id}]]";
|
return "[[bsexport:image:{$model->id}]]";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $url
|
* @property string $url
|
||||||
* @property string $path
|
* @property string $path
|
||||||
* @property string $type
|
* @property string $type
|
||||||
* @property int $uploaded_to
|
* @property int|null $uploaded_to
|
||||||
* @property int $created_by
|
* @property int $created_by
|
||||||
* @property int $updated_by
|
* @property int $updated_by
|
||||||
*/
|
*/
|
||||||
class Image extends Model implements OwnableInterface
|
class Image extends Model implements OwnableInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class ImageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy an image along with its revisions, thumbnails and remaining folders.
|
* Destroy an image along with its revisions, thumbnails, and remaining folders.
|
||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@@ -252,16 +252,7 @@ class ImageService
|
|||||||
{
|
{
|
||||||
$disk = $this->storage->getDisk('gallery');
|
$disk = $this->storage->getDisk('gallery');
|
||||||
|
|
||||||
if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) {
|
return $disk->usingSecureImages() && $this->pathAccessible($imagePath);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check local_secure is active
|
|
||||||
return $disk->usingSecureImages()
|
|
||||||
// Check the image file exists
|
|
||||||
&& $disk->exists($imagePath)
|
|
||||||
// Check the file is likely an image file
|
|
||||||
&& str_starts_with($disk->mimeType($imagePath), 'image/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,16 +260,40 @@ class ImageService
|
|||||||
*/
|
*/
|
||||||
public function pathAccessible(string $imagePath): bool
|
public function pathAccessible(string $imagePath): bool
|
||||||
{
|
{
|
||||||
$disk = $this->storage->getDisk('gallery');
|
|
||||||
|
|
||||||
if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) {
|
if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check local_secure is active
|
if ($this->storage->usingSecureImages() && user()->isGuest()) {
|
||||||
return $disk->exists($imagePath)
|
return false;
|
||||||
// Check the file is likely an image file
|
}
|
||||||
&& str_starts_with($disk->mimeType($imagePath), 'image/');
|
|
||||||
|
return $this->imageFileExists($imagePath, 'gallery');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given image should be accessible to the current user.
|
||||||
|
*/
|
||||||
|
public function imageAccessible(Image $image): bool
|
||||||
|
{
|
||||||
|
if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImage($image)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->storage->usingSecureImages() && user()->isGuest()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->imageFileExists($image->path, $image->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given image path exists for the given image type and that it is likely an image file.
|
||||||
|
*/
|
||||||
|
protected function imageFileExists(string $imagePath, string $imageType): bool
|
||||||
|
{
|
||||||
|
$disk = $this->storage->getDisk($imageType);
|
||||||
|
return $disk->exists($imagePath) && str_starts_with($disk->mimeType($imagePath), 'image/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,6 +322,11 @@ class ImageService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $this->checkUserHasAccessToRelationOfImage($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkUserHasAccessToRelationOfImage(Image $image): bool
|
||||||
|
{
|
||||||
$imageType = $image->type;
|
$imageType = $image->type;
|
||||||
|
|
||||||
// Allow user or system (logo) images
|
// Allow user or system (logo) images
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ class ImageStorage
|
|||||||
return config('filesystems.images') === 'local_secure_restricted';
|
return config('filesystems.images') === 'local_secure_restricted';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if "local secure" (Fetched behind auth, either with or without permissions enforced)
|
||||||
|
* is currently active in the instance.
|
||||||
|
*/
|
||||||
|
public function usingSecureImages(): bool
|
||||||
|
{
|
||||||
|
return config('filesystems.images') === 'local_secure' || $this->usingSecureRestrictedImages();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up an image file name to be both URL and storage safe.
|
* Clean up an image file name to be both URL and storage safe.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -374,6 +374,54 @@ class ZipExportTest extends TestCase
|
|||||||
$this->assertStringContainsString("<a href=\"{$ref}\">Original URL</a><a href=\"{$ref}\">Storage URL</a>", $pageData['html']);
|
$this->assertStringContainsString("<a href=\"{$ref}\">Original URL</a><a href=\"{$ref}\">Storage URL</a>", $pageData['html']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_orphaned_images_can_be_used_on_default_local_storage()
|
||||||
|
{
|
||||||
|
$this->asEditor();
|
||||||
|
$page = $this->entities->page();
|
||||||
|
$result = $this->files->uploadGalleryImageToPage($this, $page);
|
||||||
|
$displayThumb = $result['response']->thumbs->gallery ?? '';
|
||||||
|
$page->html = '<p><img src="' . $displayThumb . '" alt="My image"></p>';
|
||||||
|
$page->save();
|
||||||
|
|
||||||
|
$image = Image::findOrFail($result['response']->id);
|
||||||
|
$image->uploaded_to = null;
|
||||||
|
$image->save();
|
||||||
|
|
||||||
|
$zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
|
||||||
|
$zipResp->assertOk();
|
||||||
|
$zip = ZipTestHelper::extractFromZipResponse($zipResp);
|
||||||
|
$pageData = $zip->data['page'];
|
||||||
|
|
||||||
|
$this->assertCount(1, $pageData['images']);
|
||||||
|
$imageData = $pageData['images'][0];
|
||||||
|
$this->assertEquals($image->id, $imageData['id']);
|
||||||
|
|
||||||
|
$this->assertEquals('<p><img src="[[bsexport:image:' . $imageData['id'] . ']]" alt="My image"></p>', $pageData['html']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_orphaned_images_cannot_be_used_on_local_secure_restricted()
|
||||||
|
{
|
||||||
|
config()->set('filesystems.images', 'local_secure_restricted');
|
||||||
|
|
||||||
|
$this->asEditor();
|
||||||
|
$page = $this->entities->page();
|
||||||
|
$result = $this->files->uploadGalleryImageToPage($this, $page);
|
||||||
|
$displayThumb = $result['response']->thumbs->gallery ?? '';
|
||||||
|
$page->html = '<p><img src="' . $displayThumb . '" alt="My image"></p>';
|
||||||
|
$page->save();
|
||||||
|
|
||||||
|
$image = Image::findOrFail($result['response']->id);
|
||||||
|
$image->uploaded_to = null;
|
||||||
|
$image->save();
|
||||||
|
|
||||||
|
$zipResp = $this->asEditor()->get($page->getUrl("/export/zip"));
|
||||||
|
$zipResp->assertOk();
|
||||||
|
$zip = ZipTestHelper::extractFromZipResponse($zipResp);
|
||||||
|
$pageData = $zip->data['page'];
|
||||||
|
|
||||||
|
$this->assertCount(0, $pageData['images']);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_cross_reference_links_external_to_export_are_not_converted()
|
public function test_cross_reference_links_external_to_export_are_not_converted()
|
||||||
{
|
{
|
||||||
$page = $this->entities->page();
|
$page = $this->entities->page();
|
||||||
|
|||||||
Reference in New Issue
Block a user