/**
* Assigned to models that can have slugs.
* Must have the below properties.
+ *
+ * @property string $slug
*/
interface SluggableInterface
{
- /**
- * Regenerate the slug for this model.
- */
- public function refreshSlug(): string;
}
namespace BookStack\Entities\Models;
-use BookStack\References\ReferenceUpdater;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
{
return $this->belongsTo(Book::class)->withTrashed();
}
-
- /**
- * Change the book that this entity belongs to.
- */
- public function changeBook(int $newBookId): self
- {
- $oldUrl = $this->getUrl();
- $this->book_id = $newBookId;
- $this->unsetRelation('book');
- $this->refreshSlug();
- $this->save();
-
- if ($oldUrl !== $this->getUrl()) {
- app()->make(ReferenceUpdater::class)->updateEntityReferences($this, $oldUrl);
- }
-
- // Update all child pages if a chapter
- if ($this instanceof Chapter) {
- foreach ($this->pages()->withTrashed()->get() as $page) {
- $page->changeBook($newBookId);
- }
- }
-
- return $this;
- }
}
use BookStack\Activity\Models\Watch;
use BookStack\App\Model;
use BookStack\App\SluggableInterface;
-use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Permissions\JointPermissionBuilder;
use BookStack\Permissions\Models\EntityPermission;
use BookStack\Permissions\Models\JointPermission;
app()->make(SearchIndex::class)->indexEntity(clone $this);
}
- /**
- * {@inheritdoc}
- */
- public function refreshSlug(): string
- {
- $this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name);
-
- return $this->slug;
- }
-
/**
* {@inheritdoc}
*/
use BookStack\Entities\Models\HasDescriptionInterface;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\PageQueries;
+use BookStack\Entities\Tools\SlugGenerator;
+use BookStack\Entities\Tools\SlugHistory;
use BookStack\Exceptions\ImageUploadException;
use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater;
protected ReferenceStore $referenceStore,
protected PageQueries $pageQueries,
protected BookSorter $bookSorter,
+ protected SlugGenerator $slugGenerator,
+ protected SlugHistory $slugHistory,
) {
}
'updated_by' => user()->id,
'owned_by' => user()->id,
]);
- $entity->refreshSlug();
+ $this->refreshSlug($entity);
if ($entity instanceof HasDescriptionInterface) {
$this->updateDescription($entity, $input);
$entity->updated_by = user()->id;
if ($entity->isDirty('name') || empty($entity->slug)) {
- $entity->refreshSlug();
+ $this->refreshSlug($entity);
}
if ($entity instanceof HasDescriptionInterface) {
$entity->descriptionInfo()->set('', $input['description']);
}
}
+
+ /**
+ * Refresh the slug for the given entity.
+ */
+ public function refreshSlug(Entity $entity): void
+ {
+ if ($entity->id) {
+ $this->slugHistory->recordForEntity($entity);
+ }
+
+ $this->slugGenerator->regenerateForEntity($entity);
+ }
}
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Tools\ParentChanger;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\PermissionsException;
protected BaseRepo $baseRepo,
protected EntityQueries $entityQueries,
protected TrashCan $trashCan,
+ protected ParentChanger $parentChanger,
) {
}
}
return (new DatabaseTransaction(function () use ($chapter, $parent) {
- $chapter = $chapter->changeBook($parent->id);
+ $this->parentChanger->changeBook($chapter, $parent->id);
$chapter->rebuildPermissions();
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditorType;
+use BookStack\Entities\Tools\ParentChanger;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\PermissionsException;
protected ReferenceStore $referenceStore,
protected ReferenceUpdater $referenceUpdater,
protected TrashCan $trashCan,
+ protected ParentChanger $parentChanger,
) {
}
}
$page->updated_by = user()->id;
- $page->refreshSlug();
+ $this->baseRepo->refreshSlug($page);
$page->save();
$page->indexForSearch();
$this->referenceStore->updateForEntity($page);
return (new DatabaseTransaction(function () use ($page, $parent) {
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
- $page = $page->changeBook($newBookId);
+ $this->parentChanger->changeBook($page, $newBookId);
$page->rebuildPermissions();
Activity::add(ActivityType::PAGE_MOVE, $page);
protected BookRepo $bookRepo,
protected BookshelfRepo $shelfRepo,
protected Cloner $cloner,
- protected TrashCan $trashCan
+ protected TrashCan $trashCan,
+ protected ParentChanger $parentChanger,
) {
}
foreach ($chapter->pages as $page) {
$page->chapter_id = 0;
$page->save();
- $page->changeBook($book->id);
+ $this->parentChanger->changeBook($page, $book->id);
}
$this->trashCan->destroyEntity($chapter);
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Chapter;
+use BookStack\References\ReferenceUpdater;
+
+class ParentChanger
+{
+ public function __construct(
+ protected SlugGenerator $slugGenerator,
+ protected ReferenceUpdater $referenceUpdater
+ ) {
+ }
+
+ /**
+ * Change the parent book of a chapter or page.
+ */
+ public function changeBook(BookChild $child, int $newBookId): void
+ {
+ $oldUrl = $child->getUrl();
+
+ $child->book_id = $newBookId;
+ $child->unsetRelation('book');
+ $this->slugGenerator->regenerateForEntity($child);
+ $child->save();
+
+ if ($oldUrl !== $child->getUrl()) {
+ $this->referenceUpdater->updateEntityReferences($child, $oldUrl);
+ }
+
+ // Update all child pages if a chapter
+ if ($child instanceof Chapter) {
+ foreach ($child->pages()->withTrashed()->get() as $page) {
+ $this->changeBook($page, $newBookId);
+ }
+ }
+ }
+}
use BookStack\App\Model;
use BookStack\App\SluggableInterface;
use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Entity;
+use BookStack\Users\Models\User;
use Illuminate\Support\Str;
class SlugGenerator
{
/**
- * Generate a fresh slug for the given entity.
+ * Generate a fresh slug for the given item.
* The slug will be generated so that it doesn't conflict within the same parent item.
*/
public function generate(SluggableInterface&Model $model, string $slugSource): string
return $slug;
}
+ /**
+ * Regenerate the slug for the given entity.
+ */
+ public function regenerateForEntity(Entity $entity): string
+ {
+ $entity->slug = $this->generate($entity, $entity->name);
+
+ return $entity->slug;
+ }
+
+ /**
+ * Regenerate the slug for a user.
+ */
+ public function regenerateForUser(User $user): string
+ {
+ $user->slug = $this->generate($user, $user->name);
+
+ return $user->slug;
+ }
+
/**
* Format a name as a URL slug.
*/
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\Models\Entity;
+use Illuminate\Support\Facades\DB;
+
+class SlugHistory
+{
+ /**
+ * Record the current slugs for the given entity.
+ */
+ public function recordForEntity(Entity $entity): void
+ {
+ $latest = $this->getLatestEntryForEntity($entity);
+ if ($latest && $latest->slug === $entity->slug && $latest->parent_slug === $entity->getParent()?->slug) {
+ return;
+ }
+
+ $info = [
+ 'sluggable_type' => $entity->getMorphClass(),
+ 'sluggable_id' => $entity->id,
+ 'slug' => $entity->slug,
+ 'parent_slug' => $entity->getParent()?->slug,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ];
+
+ DB::table('slug_history')->insert($info);
+ }
+
+ protected function getLatestEntryForEntity(Entity $entity): \stdClass|null
+ {
+ return DB::table('slug_history')
+ ->where('sluggable_type', '=', $entity->getMorphClass())
+ ->where('sluggable_id', '=', $entity->id)
+ ->orderBy('created_at', 'desc')
+ ->first();
+ }
+}
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
+use BookStack\Entities\Tools\ParentChanger;
use BookStack\Permissions\Permission;
class BookSorter
{
public function __construct(
protected EntityQueries $queries,
+ protected ParentChanger $parentChanger,
) {
}
// Action the required changes
if ($bookChanged) {
- $model = $model->changeBook($newBook->id);
+ $this->parentChanger->changeBook($model, $newBook->id);
}
if ($model instanceof Page && $chapterChanged) {
use BookStack\Api\ApiToken;
use BookStack\App\Model;
use BookStack\App\SluggableInterface;
-use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Permissions\Permission;
use BookStack\Translation\LocaleDefinition;
use BookStack\Translation\LocaleManager;
{
return "({$this->id}) {$this->name}";
}
-
- /**
- * {@inheritdoc}
- */
- public function refreshSlug(): string
- {
- $this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name);
-
- return $this->slug;
- }
}
use BookStack\Access\UserInviteException;
use BookStack\Access\UserInviteService;
use BookStack\Activity\ActivityType;
+use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Facades\Activity;
{
public function __construct(
protected UserAvatars $userAvatar,
- protected UserInviteService $inviteService
+ protected UserInviteService $inviteService,
+ protected SlugGenerator $slugGenerator,
) {
}
$user->email_confirmed = $emailConfirmed;
$user->external_auth_id = $data['external_auth_id'] ?? '';
- $user->refreshSlug();
+ $this->slugGenerator->regenerateForUser($user);
$user->save();
if (!empty($data['language'])) {
{
if (!empty($data['name'])) {
$user->name = $data['name'];
- $user->refreshSlug();
+ $this->slugGenerator->regenerateForUser($user);
}
if (!empty($data['email']) && $manageUsersAllowed) {