Skip to content

Commit 0a182a4

Browse files
committed
ZIP Exports: Added detection/handling of images with external storage
Added test to cover.
1 parent 95d62e7 commit 0a182a4

File tree

4 files changed

+85
-7
lines changed

4 files changed

+85
-7
lines changed

app/Exports/ZipExports/ZipReferenceParser.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use BookStack\References\ModelResolvers\ImageModelResolver;
1212
use BookStack\References\ModelResolvers\PageLinkModelResolver;
1313
use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
14+
use BookStack\Uploads\ImageStorage;
1415

1516
class ZipReferenceParser
1617
{
@@ -33,8 +34,7 @@ public function __construct(
3334
*/
3435
public function parseLinks(string $content, callable $handler): string
3536
{
36-
$escapedBase = preg_quote(url('/'), '/');
37-
$linkRegex = "/({$escapedBase}.*?)[\\t\\n\\f>\"'=?#()]/";
37+
$linkRegex = $this->getLinkRegex();
3838
$matches = [];
3939
preg_match_all($linkRegex, $content, $matches);
4040

@@ -118,4 +118,23 @@ protected function getModelResolvers(): array
118118

119119
return $this->modelResolvers;
120120
}
121+
122+
/**
123+
* Build the regex to identify links we should handle in content.
124+
*/
125+
protected function getLinkRegex(): string
126+
{
127+
$urls = [rtrim(url('/'), '/')];
128+
$imageUrl = rtrim(ImageStorage::getPublicUrl('/'), '/');
129+
if ($urls[0] !== $imageUrl) {
130+
$urls[] = $imageUrl;
131+
}
132+
133+
134+
$urlBaseRegex = implode('|', array_map(function ($url) {
135+
return preg_quote($url, '/');
136+
}, $urls));
137+
138+
return "/(({$urlBaseRegex}).*?)[\\t\\n\\f>\"'=?#()]/";
139+
}
121140
}

app/References/ModelResolvers/ImageModelResolver.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@
33
namespace BookStack\References\ModelResolvers;
44

55
use BookStack\Uploads\Image;
6+
use BookStack\Uploads\ImageStorage;
67

78
class ImageModelResolver implements CrossLinkModelResolver
89
{
10+
protected ?string $pattern = null;
11+
912
public function resolve(string $link): ?Image
1013
{
11-
$pattern = '/^' . preg_quote(url('/uploads/images'), '/') . '\/(.+)/';
14+
$pattern = $this->getUrlPattern();
1215
$matches = [];
1316
$match = preg_match($pattern, $link, $matches);
1417
if (!$match) {
1518
return null;
1619
}
1720

18-
$path = $matches[1];
21+
$path = $matches[2];
1922

2023
// Strip thumbnail element from path if existing
2124
$originalPathSplit = array_filter(explode('/', $path), function (string $part) {
@@ -30,4 +33,26 @@ public function resolve(string $link): ?Image
3033

3134
return Image::query()->where('path', '=', $fullPath)->first();
3235
}
36+
37+
/**
38+
* Get the regex pattern to identify image URLs.
39+
* Caches the pattern since it requires looking up to settings/config.
40+
*/
41+
protected function getUrlPattern(): string
42+
{
43+
if ($this->pattern) {
44+
return $this->pattern;
45+
}
46+
47+
$urls = [url('/uploads/images')];
48+
$baseImageUrl = ImageStorage::getPublicUrl('/uploads/images');
49+
if ($baseImageUrl !== $urls[0]) {
50+
$urls[] = $baseImageUrl;
51+
}
52+
53+
$imageUrlRegex = implode('|', array_map(fn ($url) => preg_quote($url, '/'), $urls));
54+
$this->pattern = '/^(' . $imageUrlRegex . ')\/(.+)/';
55+
56+
return $this->pattern;
57+
}
3358
}

app/Uploads/ImageStorage.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,20 @@ public function urlToPath(string $url): ?string
110110
}
111111

112112
/**
113-
* Gets a public facing url for an image by checking relevant environment variables.
113+
* Gets a public facing url for an image or location at the given path.
114+
*/
115+
public static function getPublicUrl(string $filePath): string
116+
{
117+
return static::getPublicBaseUrl() . '/' . ltrim($filePath, '/');
118+
}
119+
120+
/**
121+
* Get the public base URL used for images.
122+
* Will not include any path element of the image file, just the base part
123+
* from where the path is then expected to start from.
114124
* If s3-style store is in use it will default to guessing a public bucket URL.
115125
*/
116-
public function getPublicUrl(string $filePath): string
126+
protected static function getPublicBaseUrl(): string
117127
{
118128
$storageUrl = config('filesystems.url');
119129

@@ -131,6 +141,6 @@ public function getPublicUrl(string $filePath): string
131141

132142
$basePath = $storageUrl ?: url('/');
133143

134-
return rtrim($basePath, '/') . $filePath;
144+
return rtrim($basePath, '/');
135145
}
136146
}

tests/Exports/ZipExportTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,30 @@ public function test_book_and_chapter_description_links_to_images_in_pages_are_c
300300
$this->assertStringContainsString('href="[[bsexport:image:' . $image->id . ']]"', $chapterData['description_html']);
301301
}
302302

303+
public function test_image_links_are_handled_when_using_external_storage_url()
304+
{
305+
$page = $this->entities->page();
306+
307+
$this->asEditor();
308+
$this->files->uploadGalleryImageToPage($this, $page);
309+
/** @var Image $image */
310+
$image = Image::query()->where('type', '=', 'gallery')
311+
->where('uploaded_to', '=', $page->id)->first();
312+
313+
config()->set('filesystems.url', 'https://i.example.com/content');
314+
315+
$storageUrl = 'https://i.example.com/content/' . ltrim($image->path, '/');
316+
$page->html = '<p><a href="' . $image->url . '">Original URL</a><a href="' . $storageUrl . '">Storage URL</a></p>';
317+
$page->save();
318+
319+
$zipResp = $this->get($page->getUrl("/export/zip"));
320+
$zip = $this->extractZipResponse($zipResp);
321+
$pageData = $zip->data['page'];
322+
323+
$ref = '[[bsexport:image:' . $image->id . ']]';
324+
$this->assertStringContainsString("<a href=\"{$ref}\">Original URL</a><a href=\"{$ref}\">Storage URL</a>", $pageData['html']);
325+
}
326+
303327
public function test_cross_reference_links_external_to_export_are_not_converted()
304328
{
305329
$page = $this->entities->page();

0 commit comments

Comments
 (0)