From 1e34954554d672446693bcc74bf7f9aae1936802 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 2 Sep 2025 11:10:47 +0100 Subject: [PATCH] Maintenance: Continued work towards PHPstan level 2 Updated html description code to be behind a proper interface. Set new convention for mode traits/interfaces. --- app/Entities/Models/Book.php | 5 +-- app/Entities/Models/Bookshelf.php | 4 +-- app/Entities/Models/Chapter.php | 4 +-- ...CoverImage.php => CoverImageInterface.php} | 2 +- .../{Deletable.php => DeletableInterface.php} | 2 +- app/Entities/Models/Deletion.php | 2 +- app/Entities/Models/Entity.php | 2 +- app/Entities/Models/HasHtmlDescription.php | 21 ----------- .../Models/HtmlDescriptionInterface.php | 17 +++++++++ app/Entities/Models/HtmlDescriptionTrait.php | 35 +++++++++++++++++++ app/Entities/Queries/PageQueries.php | 3 ++ app/Entities/Repos/BaseRepo.php | 20 +++++------ app/Entities/Tools/Cloner.php | 4 +-- app/Entities/Tools/PageIncludeParser.php | 7 ++-- app/Entities/Tools/TrashCan.php | 4 +-- app/References/ReferenceUpdater.php | 13 ++++--- app/Sorting/SortRuleController.php | 2 +- .../Controllers/ImageGalleryApiController.php | 4 +-- 18 files changed, 94 insertions(+), 57 deletions(-) rename app/Entities/Models/{HasCoverImage.php => CoverImageInterface.php} (92%) rename app/Entities/Models/{Deletable.php => DeletableInterface.php} (90%) delete mode 100644 app/Entities/Models/HasHtmlDescription.php create mode 100644 app/Entities/Models/HtmlDescriptionInterface.php create mode 100644 app/Entities/Models/HtmlDescriptionTrait.php diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index ede4fc7d5..88e3b85ba 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -26,10 +26,10 @@ use Illuminate\Support\Collection; * @property ?Page $defaultTemplate * @property ?SortRule $sortRule */ -class Book extends Entity implements HasCoverImage +class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterface { use HasFactory; - use HasHtmlDescription; + use HtmlDescriptionTrait; public float $searchFactor = 1.2; @@ -111,6 +111,7 @@ class Book extends Entity implements HasCoverImage /** * Get all chapters within this book. + * @return HasMany */ public function chapters(): HasMany { diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index 9ffa0ea9c..5b403d9c0 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -class Bookshelf extends Entity implements HasCoverImage +class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionInterface { use HasFactory; - use HasHtmlDescription; + use HtmlDescriptionTrait; protected $table = 'bookshelves'; diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index 088d199da..03e819c06 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -14,10 +14,10 @@ use Illuminate\Support\Collection; * @property ?int $default_template_id * @property ?Page $defaultTemplate */ -class Chapter extends BookChild +class Chapter extends BookChild implements HtmlDescriptionInterface { use HasFactory; - use HasHtmlDescription; + use HtmlDescriptionTrait; public float $searchFactor = 1.2; diff --git a/app/Entities/Models/HasCoverImage.php b/app/Entities/Models/CoverImageInterface.php similarity index 92% rename from app/Entities/Models/HasCoverImage.php rename to app/Entities/Models/CoverImageInterface.php index f665efce6..5f781fe02 100644 --- a/app/Entities/Models/HasCoverImage.php +++ b/app/Entities/Models/CoverImageInterface.php @@ -4,7 +4,7 @@ namespace BookStack\Entities\Models; use Illuminate\Database\Eloquent\Relations\BelongsTo; -interface HasCoverImage +interface CoverImageInterface { /** * Get the cover image for this item. diff --git a/app/Entities/Models/Deletable.php b/app/Entities/Models/DeletableInterface.php similarity index 90% rename from app/Entities/Models/Deletable.php rename to app/Entities/Models/DeletableInterface.php index a2c7fad81..f771d9c69 100644 --- a/app/Entities/Models/Deletable.php +++ b/app/Entities/Models/DeletableInterface.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; * A model that can be deleted in a manner that deletions * are tracked to be part of the recycle bin system. */ -interface Deletable +interface DeletableInterface { public function deletions(): MorphMany; } diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php index a73437c94..55589f61e 100644 --- a/app/Entities/Models/Deletion.php +++ b/app/Entities/Models/Deletion.php @@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; * @property int $deleted_by * @property string $deletable_type * @property int $deletable_id - * @property Deletable $deletable + * @property DeletableInterface $deletable */ class Deletion extends Model implements Loggable { diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 1ef4e618d..46b29f93b 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -49,7 +49,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static Builder withLastView() * @method static Builder withViewCount() */ -abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable, Loggable +abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, DeletableInterface, Loggable { use SoftDeletes; use HasCreatorAndUpdater; diff --git a/app/Entities/Models/HasHtmlDescription.php b/app/Entities/Models/HasHtmlDescription.php deleted file mode 100644 index c9f08616d..000000000 --- a/app/Entities/Models/HasHtmlDescription.php +++ /dev/null @@ -1,21 +0,0 @@ -description_html ?: '

' . nl2br(e($this->description)) . '

'; - return HtmlContentFilter::removeScriptsFromHtmlString($html); - } -} diff --git a/app/Entities/Models/HtmlDescriptionInterface.php b/app/Entities/Models/HtmlDescriptionInterface.php new file mode 100644 index 000000000..ffe7f0c5f --- /dev/null +++ b/app/Entities/Models/HtmlDescriptionInterface.php @@ -0,0 +1,17 @@ +description_html ?: '

' . nl2br(e($this->description)) . '

'; + if ($raw) { + return $html; + } + + return HtmlContentFilter::removeScriptsFromHtmlString($html); + } + + public function setDescriptionHtml(string $html, string|null $plaintext = null): void + { + $this->description_html = $html; + + if ($plaintext !== null) { + $this->description = $plaintext; + } + + if (empty($html) && !empty($plaintext)) { + $this->description_html = $this->descriptionHtml(); + } + } +} diff --git a/app/Entities/Queries/PageQueries.php b/app/Entities/Queries/PageQueries.php index 06298f470..f821ee86a 100644 --- a/app/Entities/Queries/PageQueries.php +++ b/app/Entities/Queries/PageQueries.php @@ -66,6 +66,9 @@ class PageQueries implements ProvidesEntityQueries }); } + /** + * @return Builder + */ public function visibleForList(): Builder { return $this->start() diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index ac5a44e67..f4d05d8af 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -7,8 +7,9 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasCoverImage; -use BookStack\Entities\Models\HasHtmlDescription; +use BookStack\Entities\Models\CoverImageInterface; +use BookStack\Entities\Models\HtmlDescriptionInterface; +use BookStack\Entities\Models\HtmlDescriptionTrait; use BookStack\Entities\Queries\PageQueries; use BookStack\Exceptions\ImageUploadException; use BookStack\References\ReferenceStore; @@ -88,7 +89,7 @@ class BaseRepo /** * Update the given items' cover image, or clear it. * - * @param Entity&HasCoverImage $entity + * @param Entity&CoverImageInterface $entity * * @throws ImageUploadException * @throws \Exception @@ -150,18 +151,17 @@ class BaseRepo protected function updateDescription(Entity $entity, array $input): void { - if (!in_array(HasHtmlDescription::class, class_uses($entity))) { + if (!($entity instanceof HtmlDescriptionInterface)) { return; } - /** @var HasHtmlDescription $entity */ if (isset($input['description_html'])) { - $entity->description_html = HtmlDescriptionFilter::filterFromString($input['description_html']); - $entity->description = html_entity_decode(strip_tags($input['description_html'])); + $entity->setDescriptionHtml( + HtmlDescriptionFilter::filterFromString($input['description_html']), + html_entity_decode(strip_tags($input['description_html'])) + ); } else if (isset($input['description'])) { - $entity->description = $input['description']; - $entity->description_html = ''; - $entity->description_html = $entity->descriptionHtml(); + $entity->setDescriptionHtml('', $input['description']); } } } diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index 2be6083e3..87aa770c0 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -7,7 +7,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasCoverImage; +use BookStack\Entities\Models\CoverImageInterface; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\ChapterRepo; @@ -105,7 +105,7 @@ class Cloner $inputData['tags'] = $this->entityTagsToInputArray($entity); // Add a cover to the data if existing on the original entity - if ($entity instanceof HasCoverImage) { + if ($entity instanceof CoverImageInterface) { $cover = $entity->cover()->first(); if ($cover) { $inputData['image'] = $this->imageToUploadedFile($cover); diff --git a/app/Entities/Tools/PageIncludeParser.php b/app/Entities/Tools/PageIncludeParser.php index e0b89f158..329a7633f 100644 --- a/app/Entities/Tools/PageIncludeParser.php +++ b/app/Entities/Tools/PageIncludeParser.php @@ -7,7 +7,6 @@ use Closure; use DOMDocument; use DOMElement; use DOMNode; -use DOMText; class PageIncludeParser { @@ -159,7 +158,7 @@ class PageIncludeParser /** * Splits the given $parentNode at the location of the $domNode within it. - * Attempts replicate the original $parentNode, moving some of their parent + * Attempts to replicate the original $parentNode, moving some of their parent * children in where needed, before adding the $domNode between. */ protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void @@ -171,6 +170,10 @@ class PageIncludeParser } $parentClone = $parentNode->cloneNode(); + if (!($parentClone instanceof DOMElement)) { + return; + } + $parentNode->parentNode->insertBefore($parentClone, $parentNode); $parentClone->removeAttribute('id'); diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 5e8a93719..d457d4f48 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -8,7 +8,7 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasCoverImage; +use BookStack\Entities\Models\CoverImageInterface; use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\EntityQueries; use BookStack\Exceptions\NotifyException; @@ -398,7 +398,7 @@ class TrashCan $entity->referencesTo()->delete(); $entity->referencesFrom()->delete(); - if ($entity instanceof HasCoverImage && $entity->cover()->exists()) { + if ($entity instanceof CoverImageInterface && $entity->cover()->exists()) { $imageService = app()->make(ImageService::class); $imageService->destroy($entity->cover()->first()); } diff --git a/app/References/ReferenceUpdater.php b/app/References/ReferenceUpdater.php index db355f211..5f1d711e9 100644 --- a/app/References/ReferenceUpdater.php +++ b/app/References/ReferenceUpdater.php @@ -4,7 +4,8 @@ namespace BookStack\References; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasHtmlDescription; +use BookStack\Entities\Models\HtmlDescriptionInterface; +use BookStack\Entities\Models\HtmlDescriptionTrait; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\RevisionRepo; use BookStack\Util\HtmlDocument; @@ -61,20 +62,18 @@ class ReferenceUpdater { if ($entity instanceof Page) { $this->updateReferencesWithinPage($entity, $oldLink, $newLink); - return; } - if (in_array(HasHtmlDescription::class, class_uses($entity))) { + if ($entity instanceof HtmlDescriptionInterface) { $this->updateReferencesWithinDescription($entity, $oldLink, $newLink); } } - protected function updateReferencesWithinDescription(Entity $entity, string $oldLink, string $newLink): void + protected function updateReferencesWithinDescription(Entity&HtmlDescriptionInterface $entity, string $oldLink, string $newLink): void { - /** @var HasHtmlDescription&Entity $entity */ $entity = (clone $entity)->refresh(); - $html = $this->updateLinksInHtml($entity->description_html ?: '', $oldLink, $newLink); - $entity->description_html = $html; + $html = $this->updateLinksInHtml($entity->descriptionHtml(true) ?: '', $oldLink, $newLink); + $entity->setDescriptionHtml($html); $entity->save(); } diff --git a/app/Sorting/SortRuleController.php b/app/Sorting/SortRuleController.php index 96b8e8ef5..a124ffa9c 100644 --- a/app/Sorting/SortRuleController.php +++ b/app/Sorting/SortRuleController.php @@ -29,7 +29,7 @@ class SortRuleController extends Controller $operations = SortRuleOperation::fromSequence($request->input('sequence')); if (count($operations) === 0) { - return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']); + return redirect('/settings/sorting/rules/new')->withInput()->withErrors(['sequence' => 'No operations set.']); } $rule = new SortRule(); diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index 6d4657a7a..d2a750c45 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -146,10 +146,10 @@ class ImageGalleryApiController extends ApiController $data['content']['html'] = "
id}\">
"; $data['content']['markdown'] = $data['content']['html']; } else { - $escapedDisplayThumb = htmlentities($image->thumbs['display']); + $escapedDisplayThumb = htmlentities($image->getAttribute('thumbs')['display']); $data['content']['html'] = "\"{$escapedName}\""; $mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name)); - $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display'])); + $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->getAttribute('thumbs')['display'])); $data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})"; }