Added testing to cover.
Also added batch recording of child slug pairs on book slug changes.
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Queries\BookshelfQueries;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
protected ShelfContext $shelfContext,
protected BookRepo $bookRepo,
protected BookQueries $queries,
+ protected EntityQueries $entityQueries,
protected BookshelfQueries $shelfQueries,
protected ReferenceFetcher $referenceFetcher,
) {
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
- $book = $this->queries->findVisibleBySlugOrFail($slug);
+ try {
+ $book = $this->queries->findVisibleBySlugOrFail($slug);
+ } catch (NotFoundException $exception) {
+ $book = $this->entityQueries->findVisibleByOldSlugs('book', $slug);
+ if (is_null($book)) {
+ throw $exception;
+ }
+ return redirect($book->getUrl());
+ }
+
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->scopes('visible')->get();
use BookStack\Activity\Models\View;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Queries\BookshelfQueries;
+use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
public function __construct(
protected BookshelfRepo $shelfRepo,
protected BookshelfQueries $queries,
+ protected EntityQueries $entityQueries,
protected BookQueries $bookQueries,
protected ShelfContext $shelfContext,
protected ReferenceFetcher $referenceFetcher,
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
- $shelf = $this->queries->findVisibleBySlugOrFail($slug);
+ try {
+ $shelf = $this->queries->findVisibleBySlugOrFail($slug);
+ } catch (NotFoundException $exception) {
+ $shelf = $this->entityQueries->findVisibleByOldSlugs('bookshelf', $slug);
+ if (is_null($shelf)) {
+ throw $exception;
+ }
+ return redirect($shelf->getUrl());
+ }
+
$this->checkOwnablePermission(Permission::BookshelfView, $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
*/
public function show(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
+ try {
+ $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
+ } catch (NotFoundException $exception) {
+ $chapter = $this->entityQueries->findVisibleByOldSlugs('chapter', $chapterSlug, $bookSlug);
+ if (is_null($chapter)) {
+ throw $exception;
+ }
+ return redirect($chapter->getUrl());
+ }
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
{
/**
* Get the book this page sits in.
+ * @return BelongsTo<Book, $this>
*/
public function book(): BelongsTo
{
return $this->morphMany(Watch::class, 'watchable');
}
+ /**
+ * Get the related slug history for this entity.
+ */
+ public function slugHistory(): MorphMany
+ {
+ return $this->morphMany(SlugHistory::class, 'sluggable');
+ }
+
/**
* {@inheritdoc}
*/
namespace BookStack\Entities\Tools;
+use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\EntityTable;
use BookStack\Entities\Models\SlugHistory as SlugHistoryModel;
use BookStack\Permissions\PermissionApplicator;
+use Illuminate\Support\Facades\DB;
class SlugHistory
{
$entry = new SlugHistoryModel();
$entry->forceFill($info);
$entry->save();
+
+ if ($entity instanceof Book) {
+ $this->recordForBookChildren($entity);
+ }
+ }
+
+ protected function recordForBookChildren(Book $book): void
+ {
+ $query = EntityTable::query()
+ ->select(['type', 'id', 'slug', DB::raw("'{$book->slug}' as parent_slug"), DB::raw('now() as created_at'), DB::raw('now() as updated_at')])
+ ->where('book_id', '=', $book->id)
+ ->whereNotNull('book_id');
+
+ SlugHistoryModel::query()->insertUsing(
+ ['sluggable_type', 'sluggable_id', 'slug', 'parent_slug', 'created_at', 'updated_at'],
+ $query
+ );
}
/**
/**
* Update entity relations to remove or update outstanding connections.
*/
- protected function destroyCommonRelations(Entity $entity)
+ protected function destroyCommonRelations(Entity $entity): void
{
Activity::removeEntity($entity);
$entity->views()->delete();
$entity->watches()->delete();
$entity->referencesTo()->delete();
$entity->referencesFrom()->delete();
+ $entity->slugHistory()->delete();
if ($entity instanceof HasCoverInterface && $entity->coverInfo()->exists()) {
$imageService = app()->make(ImageService::class);
public function test_old_page_slugs_redirect_to_new_pages()
{
$page = $this->entities->page();
+ $pageUrl = $page->getUrl();
- // Need to save twice since revisions are not generated in seeder.
- $this->asAdmin()->put($page->getUrl(), [
- 'name' => 'super test',
+ $this->asAdmin()->put($pageUrl, [
+ 'name' => 'super test page',
'html' => '<p></p>',
]);
- $page->refresh();
+ $this->get($pageUrl)
+ ->assertRedirect("/books/{$page->book->slug}/page/super-test-page");
+ }
+
+ public function test_old_shelf_slugs_redirect_to_new_shelf()
+ {
+ $shelf = $this->entities->shelf();
+ $shelfUrl = $shelf->getUrl();
+
+ $this->asAdmin()->put($shelf->getUrl(), [
+ 'name' => 'super test shelf',
+ ]);
+
+ $this->get($shelfUrl)
+ ->assertRedirect("/shelves/super-test-shelf");
+ }
+
+ public function test_old_book_slugs_redirect_to_new_book()
+ {
+ $book = $this->entities->book();
+ $bookUrl = $book->getUrl();
+
+ $this->asAdmin()->put($book->getUrl(), [
+ 'name' => 'super test book',
+ ]);
+
+ $this->get($bookUrl)
+ ->assertRedirect("/books/super-test-book");
+ }
+
+ public function test_old_chapter_slugs_redirect_to_new_chapter()
+ {
+ $chapter = $this->entities->chapter();
+ $chapterUrl = $chapter->getUrl();
+
+ $this->asAdmin()->put($chapter->getUrl(), [
+ 'name' => 'super test chapter',
+ ]);
+
+ $this->get($chapterUrl)
+ ->assertRedirect("/books/{$chapter->book->slug}/chapter/super-test-chapter");
+ }
+
+ public function test_old_book_slugs_in_page_urls_redirect_to_current_page_url()
+ {
+ $page = $this->entities->page();
+ $book = $page->book;
$pageUrl = $page->getUrl();
- $this->put($pageUrl, [
- 'name' => 'super test page',
- 'html' => '<p></p>',
+ $this->asAdmin()->put($book->getUrl(), [
+ 'name' => 'super test book',
]);
$this->get($pageUrl)
- ->assertRedirect("/books/{$page->book->slug}/page/super-test-page");
+ ->assertRedirect("/books/super-test-book/page/{$page->slug}");
+ }
+
+ public function test_old_book_slugs_in_chapter_urls_redirect_to_current_chapter_url()
+ {
+ $chapter = $this->entities->chapter();
+ $book = $chapter->book;
+ $chapterUrl = $chapter->getUrl();
+
+ $this->asAdmin()->put($book->getUrl(), [
+ 'name' => 'super test book',
+ ]);
+
+ $this->get($chapterUrl)
+ ->assertRedirect("/books/super-test-book/chapter/{$chapter->slug}");
}
public function test_slugs_recorded_in_history_on_page_update()