1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-09-09 06:29:32 +03:00

Maintenance: Continued work towards PHPstan level 2

Updated html description code to be behind a proper interface.
Set new convention for mode traits/interfaces.
This commit is contained in:
Dan Brown
2025-09-02 11:10:47 +01:00
parent 5ea4e1e935
commit 1e34954554
18 changed files with 94 additions and 57 deletions

View File

@@ -26,10 +26,10 @@ use Illuminate\Support\Collection;
* @property ?Page $defaultTemplate * @property ?Page $defaultTemplate
* @property ?SortRule $sortRule * @property ?SortRule $sortRule
*/ */
class Book extends Entity implements HasCoverImage class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterface
{ {
use HasFactory; use HasFactory;
use HasHtmlDescription; use HtmlDescriptionTrait;
public float $searchFactor = 1.2; public float $searchFactor = 1.2;
@@ -111,6 +111,7 @@ class Book extends Entity implements HasCoverImage
/** /**
* Get all chapters within this book. * Get all chapters within this book.
* @return HasMany<Chapter>
*/ */
public function chapters(): HasMany public function chapters(): HasMany
{ {

View File

@@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Bookshelf extends Entity implements HasCoverImage class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionInterface
{ {
use HasFactory; use HasFactory;
use HasHtmlDescription; use HtmlDescriptionTrait;
protected $table = 'bookshelves'; protected $table = 'bookshelves';

View File

@@ -14,10 +14,10 @@ use Illuminate\Support\Collection;
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?Page $defaultTemplate * @property ?Page $defaultTemplate
*/ */
class Chapter extends BookChild class Chapter extends BookChild implements HtmlDescriptionInterface
{ {
use HasFactory; use HasFactory;
use HasHtmlDescription; use HtmlDescriptionTrait;
public float $searchFactor = 1.2; public float $searchFactor = 1.2;

View File

@@ -4,7 +4,7 @@ namespace BookStack\Entities\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
interface HasCoverImage interface CoverImageInterface
{ {
/** /**
* Get the cover image for this item. * Get the cover image for this item.

View File

@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
* A model that can be deleted in a manner that deletions * A model that can be deleted in a manner that deletions
* are tracked to be part of the recycle bin system. * are tracked to be part of the recycle bin system.
*/ */
interface Deletable interface DeletableInterface
{ {
public function deletions(): MorphMany; public function deletions(): MorphMany;
} }

View File

@@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @property int $deleted_by * @property int $deleted_by
* @property string $deletable_type * @property string $deletable_type
* @property int $deletable_id * @property int $deletable_id
* @property Deletable $deletable * @property DeletableInterface $deletable
*/ */
class Deletion extends Model implements Loggable class Deletion extends Model implements Loggable
{ {

View File

@@ -49,7 +49,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static Builder withLastView() * @method static Builder withLastView()
* @method static Builder withViewCount() * @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 SoftDeletes;
use HasCreatorAndUpdater; use HasCreatorAndUpdater;

View File

@@ -1,21 +0,0 @@
<?php
namespace BookStack\Entities\Models;
use BookStack\Util\HtmlContentFilter;
/**
* @property string $description
* @property string $description_html
*/
trait HasHtmlDescription
{
/**
* Get the HTML description for this book.
*/
public function descriptionHtml(): string
{
$html = $this->description_html ?: '<p>' . nl2br(e($this->description)) . '</p>';
return HtmlContentFilter::removeScriptsFromHtmlString($html);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace BookStack\Entities\Models;
interface HtmlDescriptionInterface
{
/**
* Get the HTML-based description for this item.
* By default, the content should be sanitised unless raw is set to true.
*/
public function descriptionHtml(bool $raw = false): string;
/**
* Set the HTML-based description for this item.
*/
public function setDescriptionHtml(string $html, string|null $plaintext = null): void;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace BookStack\Entities\Models;
use BookStack\Util\HtmlContentFilter;
/**
* @property string $description
* @property string $description_html
*/
trait HtmlDescriptionTrait
{
public function descriptionHtml(bool $raw = false): string
{
$html = $this->description_html ?: '<p>' . nl2br(e($this->description)) . '</p>';
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();
}
}
}

View File

@@ -66,6 +66,9 @@ class PageQueries implements ProvidesEntityQueries
}); });
} }
/**
* @return Builder<Page>
*/
public function visibleForList(): Builder public function visibleForList(): Builder
{ {
return $this->start() return $this->start()

View File

@@ -7,8 +7,9 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\CoverImageInterface;
use BookStack\Entities\Models\HasHtmlDescription; use BookStack\Entities\Models\HtmlDescriptionInterface;
use BookStack\Entities\Models\HtmlDescriptionTrait;
use BookStack\Entities\Queries\PageQueries; use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\References\ReferenceStore; use BookStack\References\ReferenceStore;
@@ -88,7 +89,7 @@ class BaseRepo
/** /**
* Update the given items' cover image, or clear it. * Update the given items' cover image, or clear it.
* *
* @param Entity&HasCoverImage $entity * @param Entity&CoverImageInterface $entity
* *
* @throws ImageUploadException * @throws ImageUploadException
* @throws \Exception * @throws \Exception
@@ -150,18 +151,17 @@ class BaseRepo
protected function updateDescription(Entity $entity, array $input): void protected function updateDescription(Entity $entity, array $input): void
{ {
if (!in_array(HasHtmlDescription::class, class_uses($entity))) { if (!($entity instanceof HtmlDescriptionInterface)) {
return; return;
} }
/** @var HasHtmlDescription $entity */
if (isset($input['description_html'])) { if (isset($input['description_html'])) {
$entity->description_html = HtmlDescriptionFilter::filterFromString($input['description_html']); $entity->setDescriptionHtml(
$entity->description = html_entity_decode(strip_tags($input['description_html'])); HtmlDescriptionFilter::filterFromString($input['description_html']),
html_entity_decode(strip_tags($input['description_html']))
);
} else if (isset($input['description'])) { } else if (isset($input['description'])) {
$entity->description = $input['description']; $entity->setDescriptionHtml('', $input['description']);
$entity->description_html = '';
$entity->description_html = $entity->descriptionHtml();
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\CoverImageInterface;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\ChapterRepo; use BookStack\Entities\Repos\ChapterRepo;
@@ -105,7 +105,7 @@ class Cloner
$inputData['tags'] = $this->entityTagsToInputArray($entity); $inputData['tags'] = $this->entityTagsToInputArray($entity);
// Add a cover to the data if existing on the original 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(); $cover = $entity->cover()->first();
if ($cover) { if ($cover) {
$inputData['image'] = $this->imageToUploadedFile($cover); $inputData['image'] = $this->imageToUploadedFile($cover);

View File

@@ -7,7 +7,6 @@ use Closure;
use DOMDocument; use DOMDocument;
use DOMElement; use DOMElement;
use DOMNode; use DOMNode;
use DOMText;
class PageIncludeParser class PageIncludeParser
{ {
@@ -159,7 +158,7 @@ class PageIncludeParser
/** /**
* Splits the given $parentNode at the location of the $domNode within it. * 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. * children in where needed, before adding the $domNode between.
*/ */
protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void
@@ -171,6 +170,10 @@ class PageIncludeParser
} }
$parentClone = $parentNode->cloneNode(); $parentClone = $parentNode->cloneNode();
if (!($parentClone instanceof DOMElement)) {
return;
}
$parentNode->parentNode->insertBefore($parentClone, $parentNode); $parentNode->parentNode->insertBefore($parentClone, $parentNode);
$parentClone->removeAttribute('id'); $parentClone->removeAttribute('id');

View File

@@ -8,7 +8,7 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\CoverImageInterface;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\NotifyException;
@@ -398,7 +398,7 @@ class TrashCan
$entity->referencesTo()->delete(); $entity->referencesTo()->delete();
$entity->referencesFrom()->delete(); $entity->referencesFrom()->delete();
if ($entity instanceof HasCoverImage && $entity->cover()->exists()) { if ($entity instanceof CoverImageInterface && $entity->cover()->exists()) {
$imageService = app()->make(ImageService::class); $imageService = app()->make(ImageService::class);
$imageService->destroy($entity->cover()->first()); $imageService->destroy($entity->cover()->first());
} }

View File

@@ -4,7 +4,8 @@ namespace BookStack\References;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity; 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\Models\Page;
use BookStack\Entities\Repos\RevisionRepo; use BookStack\Entities\Repos\RevisionRepo;
use BookStack\Util\HtmlDocument; use BookStack\Util\HtmlDocument;
@@ -61,20 +62,18 @@ class ReferenceUpdater
{ {
if ($entity instanceof Page) { if ($entity instanceof Page) {
$this->updateReferencesWithinPage($entity, $oldLink, $newLink); $this->updateReferencesWithinPage($entity, $oldLink, $newLink);
return;
} }
if (in_array(HasHtmlDescription::class, class_uses($entity))) { if ($entity instanceof HtmlDescriptionInterface) {
$this->updateReferencesWithinDescription($entity, $oldLink, $newLink); $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(); $entity = (clone $entity)->refresh();
$html = $this->updateLinksInHtml($entity->description_html ?: '', $oldLink, $newLink); $html = $this->updateLinksInHtml($entity->descriptionHtml(true) ?: '', $oldLink, $newLink);
$entity->description_html = $html; $entity->setDescriptionHtml($html);
$entity->save(); $entity->save();
} }

View File

@@ -29,7 +29,7 @@ class SortRuleController extends Controller
$operations = SortRuleOperation::fromSequence($request->input('sequence')); $operations = SortRuleOperation::fromSequence($request->input('sequence'));
if (count($operations) === 0) { 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(); $rule = new SortRule();

View File

@@ -146,10 +146,10 @@ class ImageGalleryApiController extends ApiController
$data['content']['html'] = "<div drawio-diagram=\"{$image->id}\"><img src=\"{$escapedUrl}\"></div>"; $data['content']['html'] = "<div drawio-diagram=\"{$image->id}\"><img src=\"{$escapedUrl}\"></div>";
$data['content']['markdown'] = $data['content']['html']; $data['content']['markdown'] = $data['content']['html'];
} else { } else {
$escapedDisplayThumb = htmlentities($image->thumbs['display']); $escapedDisplayThumb = htmlentities($image->getAttribute('thumbs')['display']);
$data['content']['html'] = "<a href=\"{$escapedUrl}\" target=\"_blank\"><img src=\"{$escapedDisplayThumb}\" alt=\"{$escapedName}\"></a>"; $data['content']['html'] = "<a href=\"{$escapedUrl}\" target=\"_blank\"><img src=\"{$escapedDisplayThumb}\" alt=\"{$escapedName}\"></a>";
$mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name)); $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})"; $data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})";
} }