mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-03-30 11:37:30 +08:00
ZIP Exports: Added detection/handling of images with external storage
Added test to cover.
This commit is contained in:
parent
95d62e7f57
commit
0a182a45ba
@ -11,6 +11,7 @@ use BookStack\References\ModelResolvers\CrossLinkModelResolver;
|
|||||||
use BookStack\References\ModelResolvers\ImageModelResolver;
|
use BookStack\References\ModelResolvers\ImageModelResolver;
|
||||||
use BookStack\References\ModelResolvers\PageLinkModelResolver;
|
use BookStack\References\ModelResolvers\PageLinkModelResolver;
|
||||||
use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
|
use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
|
||||||
|
use BookStack\Uploads\ImageStorage;
|
||||||
|
|
||||||
class ZipReferenceParser
|
class ZipReferenceParser
|
||||||
{
|
{
|
||||||
@ -33,8 +34,7 @@ class ZipReferenceParser
|
|||||||
*/
|
*/
|
||||||
public function parseLinks(string $content, callable $handler): string
|
public function parseLinks(string $content, callable $handler): string
|
||||||
{
|
{
|
||||||
$escapedBase = preg_quote(url('/'), '/');
|
$linkRegex = $this->getLinkRegex();
|
||||||
$linkRegex = "/({$escapedBase}.*?)[\\t\\n\\f>\"'=?#()]/";
|
|
||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match_all($linkRegex, $content, $matches);
|
preg_match_all($linkRegex, $content, $matches);
|
||||||
|
|
||||||
@ -118,4 +118,23 @@ class ZipReferenceParser
|
|||||||
|
|
||||||
return $this->modelResolvers;
|
return $this->modelResolvers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the regex to identify links we should handle in content.
|
||||||
|
*/
|
||||||
|
protected function getLinkRegex(): string
|
||||||
|
{
|
||||||
|
$urls = [rtrim(url('/'), '/')];
|
||||||
|
$imageUrl = rtrim(ImageStorage::getPublicUrl('/'), '/');
|
||||||
|
if ($urls[0] !== $imageUrl) {
|
||||||
|
$urls[] = $imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$urlBaseRegex = implode('|', array_map(function ($url) {
|
||||||
|
return preg_quote($url, '/');
|
||||||
|
}, $urls));
|
||||||
|
|
||||||
|
return "/(({$urlBaseRegex}).*?)[\\t\\n\\f>\"'=?#()]/";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,22 @@
|
|||||||
namespace BookStack\References\ModelResolvers;
|
namespace BookStack\References\ModelResolvers;
|
||||||
|
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
|
use BookStack\Uploads\ImageStorage;
|
||||||
|
|
||||||
class ImageModelResolver implements CrossLinkModelResolver
|
class ImageModelResolver implements CrossLinkModelResolver
|
||||||
{
|
{
|
||||||
|
protected ?string $pattern = null;
|
||||||
|
|
||||||
public function resolve(string $link): ?Image
|
public function resolve(string $link): ?Image
|
||||||
{
|
{
|
||||||
$pattern = '/^' . preg_quote(url('/uploads/images'), '/') . '\/(.+)/';
|
$pattern = $this->getUrlPattern();
|
||||||
$matches = [];
|
$matches = [];
|
||||||
$match = preg_match($pattern, $link, $matches);
|
$match = preg_match($pattern, $link, $matches);
|
||||||
if (!$match) {
|
if (!$match) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $matches[1];
|
$path = $matches[2];
|
||||||
|
|
||||||
// Strip thumbnail element from path if existing
|
// Strip thumbnail element from path if existing
|
||||||
$originalPathSplit = array_filter(explode('/', $path), function (string $part) {
|
$originalPathSplit = array_filter(explode('/', $path), function (string $part) {
|
||||||
@ -30,4 +33,26 @@ class ImageModelResolver implements CrossLinkModelResolver
|
|||||||
|
|
||||||
return Image::query()->where('path', '=', $fullPath)->first();
|
return Image::query()->where('path', '=', $fullPath)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the regex pattern to identify image URLs.
|
||||||
|
* Caches the pattern since it requires looking up to settings/config.
|
||||||
|
*/
|
||||||
|
protected function getUrlPattern(): string
|
||||||
|
{
|
||||||
|
if ($this->pattern) {
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
$urls = [url('/uploads/images')];
|
||||||
|
$baseImageUrl = ImageStorage::getPublicUrl('/uploads/images');
|
||||||
|
if ($baseImageUrl !== $urls[0]) {
|
||||||
|
$urls[] = $baseImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageUrlRegex = implode('|', array_map(fn ($url) => preg_quote($url, '/'), $urls));
|
||||||
|
$this->pattern = '/^(' . $imageUrlRegex . ')\/(.+)/';
|
||||||
|
|
||||||
|
return $this->pattern;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,10 +110,20 @@ class ImageStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a public facing url for an image by checking relevant environment variables.
|
* Gets a public facing url for an image or location at the given path.
|
||||||
|
*/
|
||||||
|
public static function getPublicUrl(string $filePath): string
|
||||||
|
{
|
||||||
|
return static::getPublicBaseUrl() . '/' . ltrim($filePath, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the public base URL used for images.
|
||||||
|
* Will not include any path element of the image file, just the base part
|
||||||
|
* from where the path is then expected to start from.
|
||||||
* If s3-style store is in use it will default to guessing a public bucket URL.
|
* If s3-style store is in use it will default to guessing a public bucket URL.
|
||||||
*/
|
*/
|
||||||
public function getPublicUrl(string $filePath): string
|
protected static function getPublicBaseUrl(): string
|
||||||
{
|
{
|
||||||
$storageUrl = config('filesystems.url');
|
$storageUrl = config('filesystems.url');
|
||||||
|
|
||||||
@ -131,6 +141,6 @@ class ImageStorage
|
|||||||
|
|
||||||
$basePath = $storageUrl ?: url('/');
|
$basePath = $storageUrl ?: url('/');
|
||||||
|
|
||||||
return rtrim($basePath, '/') . $filePath;
|
return rtrim($basePath, '/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -300,6 +300,30 @@ class ZipExportTest extends TestCase
|
|||||||
$this->assertStringContainsString('href="[[bsexport:image:' . $image->id . ']]"', $chapterData['description_html']);
|
$this->assertStringContainsString('href="[[bsexport:image:' . $image->id . ']]"', $chapterData['description_html']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_image_links_are_handled_when_using_external_storage_url()
|
||||||
|
{
|
||||||
|
$page = $this->entities->page();
|
||||||
|
|
||||||
|
$this->asEditor();
|
||||||
|
$this->files->uploadGalleryImageToPage($this, $page);
|
||||||
|
/** @var Image $image */
|
||||||
|
$image = Image::query()->where('type', '=', 'gallery')
|
||||||
|
->where('uploaded_to', '=', $page->id)->first();
|
||||||
|
|
||||||
|
config()->set('filesystems.url', 'https://i.example.com/content');
|
||||||
|
|
||||||
|
$storageUrl = 'https://i.example.com/content/' . ltrim($image->path, '/');
|
||||||
|
$page->html = '<p><a href="' . $image->url . '">Original URL</a><a href="' . $storageUrl . '">Storage URL</a></p>';
|
||||||
|
$page->save();
|
||||||
|
|
||||||
|
$zipResp = $this->get($page->getUrl("/export/zip"));
|
||||||
|
$zip = $this->extractZipResponse($zipResp);
|
||||||
|
$pageData = $zip->data['page'];
|
||||||
|
|
||||||
|
$ref = '[[bsexport:image:' . $image->id . ']]';
|
||||||
|
$this->assertStringContainsString("<a href=\"{$ref}\">Original URL</a><a href=\"{$ref}\">Storage URL</a>", $pageData['html']);
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user