Dan Brown

Added smarter page finding so changing the page name does not break old urls

Added page & book slug history to revisions so they can be looked up if a page is not found.
...@@ -11,6 +11,7 @@ use BookStack\Http\Requests; ...@@ -11,6 +11,7 @@ use BookStack\Http\Requests;
11 use BookStack\Repos\BookRepo; 11 use BookStack\Repos\BookRepo;
12 use BookStack\Repos\ChapterRepo; 12 use BookStack\Repos\ChapterRepo;
13 use BookStack\Repos\PageRepo; 13 use BookStack\Repos\PageRepo;
14 +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
14 use Views; 15 use Views;
15 16
16 class PageController extends Controller 17 class PageController extends Controller
...@@ -81,6 +82,8 @@ class PageController extends Controller ...@@ -81,6 +82,8 @@ class PageController extends Controller
81 82
82 /** 83 /**
83 * Display the specified page. 84 * Display the specified page.
85 + * If the page is not found via the slug the
86 + * revisions are searched for a match.
84 * 87 *
85 * @param $bookSlug 88 * @param $bookSlug
86 * @param $pageSlug 89 * @param $pageSlug
...@@ -89,7 +92,15 @@ class PageController extends Controller ...@@ -89,7 +92,15 @@ class PageController extends Controller
89 public function show($bookSlug, $pageSlug) 92 public function show($bookSlug, $pageSlug)
90 { 93 {
91 $book = $this->bookRepo->getBySlug($bookSlug); 94 $book = $this->bookRepo->getBySlug($bookSlug);
92 - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 95 +
96 + try {
97 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
98 + } catch (NotFoundHttpException $e) {
99 + $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
100 + if ($page === null) abort(404);
101 + return redirect($page->getUrl());
102 + }
103 +
93 $sidebarTree = $this->bookRepo->getChildren($book); 104 $sidebarTree = $this->bookRepo->getChildren($book);
94 Views::add($page); 105 Views::add($page);
95 $this->setPageTitle($page->getShortName()); 106 $this->setPageTitle($page->getShortName());
......
...@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Log; ...@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Log;
10 use Illuminate\Support\Str; 10 use Illuminate\Support\Str;
11 use BookStack\Page; 11 use BookStack\Page;
12 use BookStack\PageRevision; 12 use BookStack\PageRevision;
13 +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
13 14
14 class PageRepo 15 class PageRepo
15 { 16 {
...@@ -65,11 +66,28 @@ class PageRepo ...@@ -65,11 +66,28 @@ class PageRepo
65 public function getBySlug($slug, $bookId) 66 public function getBySlug($slug, $bookId)
66 { 67 {
67 $page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); 68 $page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
68 - if ($page === null) abort(404); 69 + if ($page === null) throw new NotFoundHttpException('Page not found');
69 return $page; 70 return $page;
70 } 71 }
71 72
72 /** 73 /**
74 + * Search through page revisions and retrieve
75 + * the last page in the current book that
76 + * has a slug equal to the one given.
77 + * @param $pageSlug
78 + * @param $bookSlug
79 + * @return null | Page
80 + */
81 + public function findPageUsingOldSlug($pageSlug, $bookSlug)
82 + {
83 + $revision = $this->pageRevision->where('slug', '=', $pageSlug)
84 + ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
85 + ->with('page')->first();
86 + return $revision !== null ? $revision->page : null;
87 + }
88 +
89 + /**
90 + * Get a new Page instance from the given input.
73 * @param $input 91 * @param $input
74 * @return Page 92 * @return Page
75 */ 93 */
...@@ -245,9 +263,13 @@ class PageRepo ...@@ -245,9 +263,13 @@ class PageRepo
245 $this->saveRevision($page); 263 $this->saveRevision($page);
246 } 264 }
247 265
266 + // Prevent slug being updated if no name change
267 + if ($page->name !== $input['name']) {
268 + $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
269 + }
270 +
248 // Update with new details 271 // Update with new details
249 $page->fill($input); 272 $page->fill($input);
250 - $page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
251 $page->html = $this->formatHtml($input['html']); 273 $page->html = $this->formatHtml($input['html']);
252 $page->text = strip_tags($page->html); 274 $page->text = strip_tags($page->html);
253 $page->updated_by = auth()->user()->id; 275 $page->updated_by = auth()->user()->id;
...@@ -283,6 +305,8 @@ class PageRepo ...@@ -283,6 +305,8 @@ class PageRepo
283 { 305 {
284 $revision = $this->pageRevision->fill($page->toArray()); 306 $revision = $this->pageRevision->fill($page->toArray());
285 $revision->page_id = $page->id; 307 $revision->page_id = $page->id;
308 + $revision->slug = $page->slug;
309 + $revision->book_slug = $page->book->slug;
286 $revision->created_by = auth()->user()->id; 310 $revision->created_by = auth()->user()->id;
287 $revision->created_at = $page->updated_at; 311 $revision->created_at = $page->updated_at;
288 $revision->save(); 312 $revision->save();
......
1 +<?php
2 +
3 +use Illuminate\Database\Schema\Blueprint;
4 +use Illuminate\Database\Migrations\Migration;
5 +
6 +class AddSlugToRevisions extends Migration
7 +{
8 + /**
9 + * Run the migrations.
10 + *
11 + * @return void
12 + */
13 + public function up()
14 + {
15 + Schema::table('page_revisions', function (Blueprint $table) {
16 + $table->string('slug');
17 + $table->index('slug');
18 + $table->string('book_slug');
19 + $table->index('book_slug');
20 + });
21 + }
22 +
23 + /**
24 + * Reverse the migrations.
25 + *
26 + * @return void
27 + */
28 + public function down()
29 + {
30 + Schema::table('page_revisions', function (Blueprint $table) {
31 + $table->dropColumn('slug');
32 + $table->dropColumn('book_slug');
33 + });
34 + }
35 +}
...@@ -211,5 +211,18 @@ class EntityTest extends TestCase ...@@ -211,5 +211,18 @@ class EntityTest extends TestCase
211 ->seeInNthElement('.entity-list .page', 0, $content['page']->name); 211 ->seeInNthElement('.entity-list .page', 0, $content['page']->name);
212 } 212 }
213 213
214 + public function test_old_page_slugs_redirect_to_new_pages()
215 + {
216 + $page = \BookStack\Page::all()->first();
217 + $pageUrl = $page->getUrl();
218 + $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
219 + $this->asAdmin()->visit($pageUrl)
220 + ->clickInElement('#content', 'Edit')
221 + ->type('super test page', '#name')
222 + ->press('Save Page')
223 + ->seePageIs($newPageUrl)
224 + ->visit($pageUrl)
225 + ->seePageIs($newPageUrl);
226 + }
214 227
215 } 228 }
......