From aaa28186bc0dff12c38f4627dac2034fa9ddb63d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 18 Nov 2025 14:19:46 +0000 Subject: [PATCH 1/5] 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. --- .../ZipExports/ZipExportReferences.php | 16 ++++-- app/Uploads/Image.php | 16 +++--- app/Uploads/ImageService.php | 54 +++++++++++++------ app/Uploads/ImageStorage.php | 9 ++++ tests/Exports/ZipExportTest.php | 48 +++++++++++++++++ 5 files changed, 115 insertions(+), 28 deletions(-) diff --git a/app/Exports/ZipExports/ZipExportReferences.php b/app/Exports/ZipExports/ZipExportReferences.php index 64107cf21..a79988d44 100644 --- a/app/Exports/ZipExports/ZipExportReferences.php +++ b/app/Exports/ZipExports/ZipExportReferences.php @@ -15,6 +15,7 @@ use BookStack\Exports\ZipExports\Models\ZipExportPage; use BookStack\Permissions\Permission; use BookStack\Uploads\Attachment; use BookStack\Uploads\Image; +use BookStack\Uploads\ImageService; class ZipExportReferences { @@ -33,6 +34,7 @@ class ZipExportReferences public function __construct( protected ZipReferenceParser $parser, + protected ImageService $imageService, ) { } @@ -133,10 +135,17 @@ class ZipExportReferences 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(); - $pageExportModel = $this->pages[$page->id] ?? ($exportModel instanceof ZipExportPage ? $exportModel : null); - if (isset($this->images[$model->id]) || ($page && $pageExportModel && userCan(Permission::PageView, $page))) { + $pageExportModel = null; + 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])) { $exportImage = ZipExportImage::fromModel($model, $files); $this->images[$model->id] = $exportImage; @@ -144,6 +153,7 @@ class ZipExportReferences } return "[[bsexport:image:{$model->id}]]"; } + return null; } diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 20def9de6..81b6db6fd 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -13,14 +13,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; /** - * @property int $id - * @property string $name - * @property string $url - * @property string $path - * @property string $type - * @property int $uploaded_to - * @property int $created_by - * @property int $updated_by + * @property int $id + * @property string $name + * @property string $url + * @property string $path + * @property string $type + * @property int|null $uploaded_to + * @property int $created_by + * @property int $updated_by */ class Image extends Model implements OwnableInterface { diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index fadafc8e5..a26a04ac5 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -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 */ @@ -252,16 +252,7 @@ class ImageService { $disk = $this->storage->getDisk('gallery'); - if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($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/'); + return $disk->usingSecureImages() && $this->pathAccessible($imagePath); } /** @@ -269,16 +260,40 @@ class ImageService */ public function pathAccessible(string $imagePath): bool { - $disk = $this->storage->getDisk('gallery'); - if ($this->storage->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) { return false; } - // Check local_secure is active - return $disk->exists($imagePath) - // Check the file is likely an image file - && str_starts_with($disk->mimeType($imagePath), 'image/'); + if ($this->storage->usingSecureImages() && user()->isGuest()) { + return false; + } + + 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 $this->checkUserHasAccessToRelationOfImage($image); + } + + protected function checkUserHasAccessToRelationOfImage(Image $image): bool + { $imageType = $image->type; // Allow user or system (logo) images diff --git a/app/Uploads/ImageStorage.php b/app/Uploads/ImageStorage.php index ddaa26a94..38a22e3b4 100644 --- a/app/Uploads/ImageStorage.php +++ b/app/Uploads/ImageStorage.php @@ -34,6 +34,15 @@ class ImageStorage 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. */ diff --git a/tests/Exports/ZipExportTest.php b/tests/Exports/ZipExportTest.php index 692a5910f..063f6d942 100644 --- a/tests/Exports/ZipExportTest.php +++ b/tests/Exports/ZipExportTest.php @@ -374,6 +374,54 @@ class ZipExportTest extends TestCase $this->assertStringContainsString("Original URLStorage URL", $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 = '

My image

'; + $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('

My image

', $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 = '

My image

'; + $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() { $page = $this->entities->page(); From 99a1d82f0a4f101367cab842ebfd4ee062eefd3e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 18 Nov 2025 18:34:41 +0000 Subject: [PATCH 2/5] Deps: Updated PHP package versions Also updated dev version --- composer.lock | 161 ++++++++++++++++++++++++++------------------------ version | 2 +- 2 files changed, 86 insertions(+), 77 deletions(-) diff --git a/composer.lock b/composer.lock index 04561900b..282a6b239 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.359.8", + "version": "3.360.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a5be7ed5efd25d70a74275daeff896b896d9c286" + "reference": "a21055795be59f3d7c5ca6e4d52a80930dcf8c20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a5be7ed5efd25d70a74275daeff896b896d9c286", - "reference": "a5be7ed5efd25d70a74275daeff896b896d9c286", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a21055795be59f3d7c5ca6e4d52a80930dcf8c20", + "reference": "a21055795be59f3d7c5ca6e4d52a80930dcf8c20", "shasum": "" }, "require": { @@ -153,22 +153,22 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.359.8" + "source": "https://github.com/aws/aws-sdk-php/tree/3.360.0" }, - "time": "2025-11-07T19:48:19+00:00" + "time": "2025-11-17T19:46:19+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f" + "reference": "fe259c55425b8178f77fb6d1f84ba2473e21ed55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f", - "reference": "f9cc1f52b5a463062251d666761178dbdb6b544f", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/fe259c55425b8178f77fb6d1f84ba2473e21ed55", + "reference": "fe259c55425b8178f77fb6d1f84ba2473e21ed55", "shasum": "" }, "require": { @@ -178,8 +178,9 @@ }, "require-dev": { "phly/keep-a-changelog": "^2.12", - "phpunit/phpunit": "^10.5.11 || 11.0.4", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", "squizlabs/php_codesniffer": "^3.9" }, "suggest": { @@ -207,9 +208,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.2" }, - "time": "2024-10-01T13:55:55+00:00" + "time": "2025-11-16T22:59:48+00:00" }, { "name": "brick/math", @@ -1738,16 +1739,16 @@ }, { "name": "laravel/framework", - "version": "v12.37.0", + "version": "v12.39.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125" + "reference": "1a6176129ef28eaf42b6b4a6250025120c3d8dac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", - "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", + "url": "https://api.github.com/repos/laravel/framework/zipball/1a6176129ef28eaf42b6b4a6250025120c3d8dac", + "reference": "1a6176129ef28eaf42b6b4a6250025120c3d8dac", "shasum": "" }, "require": { @@ -1865,7 +1866,7 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0", + "resend/resend-php": "^0.10.0|^1.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", "symfony/psr-http-message-bridge": "^7.2.0", @@ -1899,7 +1900,7 @@ "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", @@ -1953,7 +1954,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-11-04T15:39:33+00:00" + "time": "2025-11-18T15:16:10+00:00" }, { "name": "laravel/prompts", @@ -2404,16 +2405,16 @@ }, { "name": "league/flysystem", - "version": "3.30.1", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da" + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/c139fd65c1f796b926f4aec0df37f6caa959a8da", - "reference": "c139fd65c1f796b926f4aec0df37f6caa959a8da", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", "shasum": "" }, "require": { @@ -2481,9 +2482,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" }, - "time": "2025-10-20T15:35:26+00:00" + "time": "2025-11-10T17:13:11+00:00" }, { "name": "league/flysystem-aws-s3-v3", @@ -2542,16 +2543,16 @@ }, { "name": "league/flysystem-local", - "version": "3.30.0", + "version": "3.30.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", - "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", + "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", "shasum": "" }, "require": { @@ -2585,9 +2586,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" }, - "time": "2025-05-21T10:34:19+00:00" + "time": "2025-11-10T11:23:37+00:00" }, { "name": "league/html-to-markdown", @@ -2877,33 +2878,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2931,6 +2937,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -2943,9 +2950,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -2955,7 +2964,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -2963,26 +2972,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -2990,6 +2998,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3014,7 +3023,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -3039,7 +3048,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -3047,7 +3056,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "masterminds/html5", @@ -5956,16 +5965,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.6", + "version": "v7.3.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6379e490d6ecfc5c4224ff3a754b90495ecd135c" + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6379e490d6ecfc5c4224ff3a754b90495ecd135c", - "reference": "6379e490d6ecfc5c4224ff3a754b90495ecd135c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/db488a62f98f7a81d5746f05eea63a74e55bb7c4", + "reference": "db488a62f98f7a81d5746f05eea63a74e55bb7c4", "shasum": "" }, "require": { @@ -6015,7 +6024,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.6" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.7" }, "funding": [ { @@ -6035,20 +6044,20 @@ "type": "tidelift" } ], - "time": "2025-11-06T11:05:57+00:00" + "time": "2025-11-08T16:41:12+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.6", + "version": "v7.3.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f9a34dc0196677250e3609c2fac9de9e1551a262" + "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f9a34dc0196677250e3609c2fac9de9e1551a262", - "reference": "f9a34dc0196677250e3609c2fac9de9e1551a262", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/10b8e9b748ea95fa4539c208e2487c435d3c87ce", + "reference": "10b8e9b748ea95fa4539c208e2487c435d3c87ce", "shasum": "" }, "require": { @@ -6133,7 +6142,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.6" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.7" }, "funding": [ { @@ -6153,7 +6162,7 @@ "type": "tidelift" } ], - "time": "2025-11-06T20:58:12+00:00" + "time": "2025-11-12T11:38:40+00:00" }, { "name": "symfony/mailer", @@ -8795,11 +8804,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.31", + "version": "2.1.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", - "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", + "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", "shasum": "" }, "require": { @@ -8844,7 +8853,7 @@ "type": "github" } ], - "time": "2025-10-10T14:14:11+00:00" + "time": "2025-11-11T15:18:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9183,16 +9192,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.43", + "version": "11.5.44", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c6b89b6cf4324a8b4cb86e1f5dfdd6c9e0371924" + "reference": "c346885c95423eda3f65d85a194aaa24873cda82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c6b89b6cf4324a8b4cb86e1f5dfdd6c9e0371924", - "reference": "c6b89b6cf4324a8b4cb86e1f5dfdd6c9e0371924", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", + "reference": "c346885c95423eda3f65d85a194aaa24873cda82", "shasum": "" }, "require": { @@ -9264,7 +9273,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.43" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44" }, "funding": [ { @@ -9288,7 +9297,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T08:39:39+00:00" + "time": "2025-11-13T07:17:35+00:00" }, { "name": "sebastian/cli-parser", @@ -10524,16 +10533,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -10562,7 +10571,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -10570,7 +10579,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], diff --git a/version b/version index 51b8fdb60..085e9695f 100644 --- a/version +++ b/version @@ -1 +1 @@ -v25.02-dev +v25.11-dev From 1be2969055e083fa9b193fe86646142b5d261520 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 18 Nov 2025 19:47:41 +0000 Subject: [PATCH 3/5] Dev: Set timezone for test DB creation, added PHP 8.5 to tests Also fixed some test namespaces Related to #5881 --- .github/workflows/test-migrations.yml | 2 +- .github/workflows/test-php.yml | 2 +- composer.json | 1 + database/seeders/DummyContentSeeder.php | 5 +---- dev/docs/development.md | 4 ++-- dev/docs/php-testing.md | 2 +- tests/Activity/CommentsApiTest.php | 2 +- tests/Entity/EntityQueryTest.php | 2 +- 8 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml index 23e58a772..80075c3f7 100644 --- a/.github/workflows/test-migrations.yml +++ b/.github/workflows/test-migrations.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - php: ['8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index 64132f673..5f4c16caf 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - php: ['8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v4 diff --git a/composer.json b/composer.json index 2285a22cb..ab33e87a4 100644 --- a/composer.json +++ b/composer.json @@ -93,6 +93,7 @@ "@php artisan view:clear" ], "refresh-test-database": [ + "@putenv APP_TIMEZONE=UTC", "@php artisan migrate:refresh --database=mysql_testing", "@php artisan db:seed --class=DummyContentSeeder --database=mysql_testing" ] diff --git a/database/seeders/DummyContentSeeder.php b/database/seeders/DummyContentSeeder.php index 5f787259a..845bea8e4 100644 --- a/database/seeders/DummyContentSeeder.php +++ b/database/seeders/DummyContentSeeder.php @@ -13,7 +13,6 @@ use BookStack\Search\SearchIndex; use BookStack\Users\Models\Role; use BookStack\Users\Models\User; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Seeder; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; @@ -23,10 +22,8 @@ class DummyContentSeeder extends Seeder { /** * Run the database seeds. - * - * @return void */ - public function run() + public function run(): void { // Create an editor user $editorUser = User::factory()->create(); diff --git a/dev/docs/development.md b/dev/docs/development.md index ea3e692a1..6c250902a 100644 --- a/dev/docs/development.md +++ b/dev/docs/development.md @@ -7,7 +7,7 @@ When it's time for a release the `development` branch is merged into release wit ## Building CSS & JavaScript Assets -This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks: +This project uses SASS for CSS development which is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks: ``` bash # Install NPM Dependencies @@ -113,4 +113,4 @@ docker-compose run app php vendor/bin/phpunit ### Debugging The docker-compose setup ships with Xdebug, which you can listen to on port 9090. -NB : For some editors like Visual Studio Code, you might need to map your workspace folder to the /app folder within the docker container for this to work. +NB: For some editors like Visual Studio Code, you might need to map your workspace folder to the /app folder within the docker container for this to work. diff --git a/dev/docs/php-testing.md b/dev/docs/php-testing.md index 1bd4a414f..3d3e91fa2 100644 --- a/dev/docs/php-testing.md +++ b/dev/docs/php-testing.md @@ -4,7 +4,7 @@ BookStack has many test cases defined within the `tests/` directory of the app. ## Setup -The application tests are mostly functional, rather than unit tests, meaning they simulate user actions and system components and therefore these require use of the database. To avoid potential conflicts within your development environment, the tests use a separate database. This is defined via a specific `mysql_testing` database connection in our configuration, and expects to use the following database access details: +The application tests are mostly functional, rather than unit tests, meaning they simulate user actions and system components, and therefore these require use of the database. To avoid potential conflicts within your development environment, the tests use a separate database. This is defined via a specific `mysql_testing` database connection in our configuration, and expects to use the following database access details: - Host: `127.0.0.1` - Username: `bookstack-test` diff --git a/tests/Activity/CommentsApiTest.php b/tests/Activity/CommentsApiTest.php index ec4ddba99..51009da39 100644 --- a/tests/Activity/CommentsApiTest.php +++ b/tests/Activity/CommentsApiTest.php @@ -1,6 +1,6 @@ Date: Wed, 19 Nov 2025 14:37:04 +0000 Subject: [PATCH 4/5] New translations common.php (Albanian) (#5887) --- lang/sq/common.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/sq/common.php b/lang/sq/common.php index 06a9e855c..aa2988124 100644 --- a/lang/sq/common.php +++ b/lang/sq/common.php @@ -89,8 +89,8 @@ return [ 'homepage' => 'Homepage', 'header_menu_expand' => 'Expand Header Menu', 'profile_menu' => 'Profile Menu', - 'view_profile' => 'View Profile', - 'edit_profile' => 'Edit Profile', + 'view_profile' => 'Shiko profilin', + 'edit_profile' => 'Ndrysho profilin', 'dark_mode' => 'Dark Mode', 'light_mode' => 'Light Mode', 'global_search' => 'Global Search', From 47f12cc8f6830a8e20b3c3d8efad40d7c689a217 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 19 Nov 2025 14:38:35 +0000 Subject: [PATCH 5/5] Maintenance: Fixed type issue, updated translator list --- .github/translators.txt | 1 + app/Search/SearchIndex.php | 2 +- dev/licensing/php-library-licenses.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/translators.txt b/.github/translators.txt index 0f3bf18fc..67eea4874 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -511,3 +511,4 @@ MrCharlesIII :: Arabic David Olsen (dawin) :: Danish ltnzr :: French Frank Holler (holler.frank) :: German; German Informal +Korab Arifi (korabidev) :: Albanian diff --git a/app/Search/SearchIndex.php b/app/Search/SearchIndex.php index 117d069ea..ce78831ee 100644 --- a/app/Search/SearchIndex.php +++ b/app/Search/SearchIndex.php @@ -126,7 +126,7 @@ class SearchIndex $termMap = $this->textToTermCountMap($text); foreach ($termMap as $term => $count) { - $termMap[$term] = floor($count * $scoreAdjustment); + $termMap[$term] = intval($count * $scoreAdjustment); } return $termMap; diff --git a/dev/licensing/php-library-licenses.txt b/dev/licensing/php-library-licenses.txt index 090b243d8..276f39230 100644 --- a/dev/licensing/php-library-licenses.txt +++ b/dev/licensing/php-library-licenses.txt @@ -13,7 +13,7 @@ Link: http://aws.amazon.com/sdkforphp bacon/bacon-qr-code License: BSD-2-Clause License File: vendor/bacon/bacon-qr-code/LICENSE -Copyright: Copyright (c) 2017, Ben Scholzen 'DASPRiD' +Copyright: Copyright (c) 2017-present, Ben Scholzen 'DASPRiD' All rights reserved. Source: https://github.com/Bacon/BaconQrCode.git Link: https://github.com/Bacon/BaconQrCode