]> BookStack Code Mirror - bookstack/commitdiff
ZIP Exports: Added detection/handling of images with external storage
authorDan Brown <redacted>
Tue, 26 Nov 2024 15:59:39 +0000 (15:59 +0000)
committerDan Brown <redacted>
Tue, 26 Nov 2024 15:59:39 +0000 (15:59 +0000)
Added test to cover.

app/Exports/ZipExports/ZipReferenceParser.php
app/References/ModelResolvers/ImageModelResolver.php
app/Uploads/ImageStorage.php
tests/Exports/ZipExportTest.php

index 5929383b4dd77f29248f16ac3666ea6dcffd0bc2..a6560e3f289aebd7a9fc50b1d8db14b96e9c4bc8 100644 (file)
@@ -11,6 +11,7 @@ use BookStack\References\ModelResolvers\CrossLinkModelResolver;
 use BookStack\References\ModelResolvers\ImageModelResolver;
 use BookStack\References\ModelResolvers\PageLinkModelResolver;
 use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
+use BookStack\Uploads\ImageStorage;
 
 class ZipReferenceParser
 {
@@ -33,8 +34,7 @@ class ZipReferenceParser
      */
     public function parseLinks(string $content, callable $handler): string
     {
-        $escapedBase = preg_quote(url('/'), '/');
-        $linkRegex = "/({$escapedBase}.*?)[\\t\\n\\f>\"'=?#()]/";
+        $linkRegex = $this->getLinkRegex();
         $matches = [];
         preg_match_all($linkRegex, $content, $matches);
 
@@ -118,4 +118,23 @@ class ZipReferenceParser
 
         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>\"'=?#()]/";
+    }
 }
index 331dd593b7389c1e9f0d699ecfc2f860996889ef..2c6c9fecd74b4cf33a51a334cd1136a7779c34d5 100644 (file)
@@ -3,19 +3,22 @@
 namespace BookStack\References\ModelResolvers;
 
 use BookStack\Uploads\Image;
+use BookStack\Uploads\ImageStorage;
 
 class ImageModelResolver implements CrossLinkModelResolver
 {
+    protected ?string $pattern = null;
+
     public function resolve(string $link): ?Image
     {
-        $pattern = '/^' . preg_quote(url('/uploads/images'), '/') . '\/(.+)/';
+        $pattern = $this->getUrlPattern();
         $matches = [];
         $match = preg_match($pattern, $link, $matches);
         if (!$match) {
             return null;
         }
 
-        $path = $matches[1];
+        $path = $matches[2];
 
         // Strip thumbnail element from path if existing
         $originalPathSplit = array_filter(explode('/', $path), function (string $part) {
@@ -30,4 +33,26 @@ class ImageModelResolver implements CrossLinkModelResolver
 
         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;
+    }
 }
index dc4abc0f281b6ba5fbbc6828411bd4abef40b3bb..ddaa26a9400c343b2dea6bf32dd118f1a5e79bfc 100644 (file)
@@ -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.
      */
-    public function getPublicUrl(string $filePath): string
+    protected static function getPublicBaseUrl(): string
     {
         $storageUrl = config('filesystems.url');
 
@@ -131,6 +141,6 @@ class ImageStorage
 
         $basePath = $storageUrl ?: url('/');
 
-        return rtrim($basePath, '/') . $filePath;
+        return rtrim($basePath, '/');
     }
 }
index 6e8462f596f7ebc3b85f369471e88e1949c0b484..17891c73d73f171318b3712bb36730e5bbdeae52 100644 (file)
@@ -300,6 +300,30 @@ class ZipExportTest extends TestCase
         $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()
     {
         $page = $this->entities->page();