3 namespace BookStack\Entities\Controllers;
5 use BookStack\Activity\Models\View;
6 use BookStack\Activity\Tools\CommentTree;
7 use BookStack\Activity\Tools\UserEntityWatchOptions;
8 use BookStack\Entities\Models\Book;
9 use BookStack\Entities\Models\Chapter;
10 use BookStack\Entities\Queries\EntityQueries;
11 use BookStack\Entities\Queries\PageQueries;
12 use BookStack\Entities\Repos\PageRepo;
13 use BookStack\Entities\Tools\BookContents;
14 use BookStack\Entities\Tools\Cloner;
15 use BookStack\Entities\Tools\NextPreviousContentLocator;
16 use BookStack\Entities\Tools\PageContent;
17 use BookStack\Entities\Tools\PageEditActivity;
18 use BookStack\Entities\Tools\PageEditorData;
19 use BookStack\Exceptions\NotFoundException;
20 use BookStack\Exceptions\PermissionsException;
21 use BookStack\Http\Controller;
22 use BookStack\Permissions\Permission;
23 use BookStack\References\ReferenceFetcher;
25 use Illuminate\Database\Eloquent\Relations\BelongsTo;
26 use Illuminate\Http\Request;
27 use Illuminate\Validation\ValidationException;
30 class PageController extends Controller
32 public function __construct(
33 protected PageRepo $pageRepo,
34 protected PageQueries $queries,
35 protected EntityQueries $entityQueries,
36 protected ReferenceFetcher $referenceFetcher
41 * Show the form for creating a new page.
45 public function create(string $bookSlug, ?string $chapterSlug = null)
48 $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
50 $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
53 $this->checkOwnablePermission(Permission::PageCreate, $parent);
55 // Redirect to draft edit screen if signed in
56 if ($this->isSignedIn()) {
57 $draft = $this->pageRepo->getNewDraftPage($parent);
59 return redirect($draft->getUrl());
62 // Otherwise show the edit view if they're a guest
63 $this->setPageTitle(trans('entities.pages_new'));
65 return view('pages.guest-create', ['parent' => $parent]);
69 * Create a new page as a guest user.
71 * @throws ValidationException
73 public function createAsGuest(Request $request, string $bookSlug, ?string $chapterSlug = null)
75 $this->validate($request, [
76 'name' => ['required', 'string', 'max:255'],
80 $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
82 $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
85 $this->checkOwnablePermission(Permission::PageCreate, $parent);
87 $page = $this->pageRepo->getNewDraftPage($parent);
88 $this->pageRepo->publishDraft($page, [
89 'name' => $request->get('name'),
92 return redirect($page->getUrl('/edit'));
96 * Show form to continue editing a draft page.
98 * @throws NotFoundException
100 public function editDraft(Request $request, string $bookSlug, int $pageId)
102 $draft = $this->queries->findVisibleByIdOrFail($pageId);
103 $this->checkOwnablePermission(Permission::PageCreate, $draft->getParent());
105 $editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
106 $this->setPageTitle(trans('entities.pages_edit_draft'));
108 return view('pages.edit', $editorData->getViewData());
112 * Store a new page by changing a draft into a page.
114 * @throws NotFoundException
115 * @throws ValidationException
117 public function store(Request $request, string $bookSlug, int $pageId)
119 $this->validate($request, [
120 'name' => ['required', 'string', 'max:255'],
123 $draftPage = $this->queries->findVisibleByIdOrFail($pageId);
124 $this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());
126 $page = $this->pageRepo->publishDraft($draftPage, $request->all());
128 return redirect($page->getUrl());
132 * Display the specified page.
133 * If the page is not found via the slug the revisions are searched for a match.
135 * @throws NotFoundException
137 public function show(string $bookSlug, string $pageSlug)
140 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
141 } catch (NotFoundException $e) {
142 $page = $this->entityQueries->findVisibleByOldSlugs('page', $pageSlug, $bookSlug);
143 if (is_null($page)) {
147 return redirect($page->getUrl());
150 $pageContent = (new PageContent($page));
151 $page->html = $pageContent->render();
152 $pageNav = $pageContent->getNavigation($page->html);
154 $sidebarTree = (new BookContents($page->book))->getTree();
155 $commentTree = (new CommentTree($page));
156 $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
158 View::incrementFor($page);
159 $this->setPageTitle($page->getShortName());
161 return view('pages.show', [
163 'book' => $page->book,
165 'sidebarTree' => $sidebarTree,
166 'commentTree' => $commentTree,
167 'pageNav' => $pageNav,
168 'watchOptions' => new UserEntityWatchOptions(user(), $page),
169 'next' => $nextPreviousLocator->getNext(),
170 'previous' => $nextPreviousLocator->getPrevious(),
171 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($page),
176 * Get page from an ajax request.
178 * @throws NotFoundException
180 public function getPageAjax(int $pageId)
182 $page = $this->queries->findVisibleByIdOrFail($pageId);
183 $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
184 $page->makeHidden(['book']);
186 return response()->json($page);
190 * Show the form for editing the specified page.
192 * @throws NotFoundException
194 public function edit(Request $request, string $bookSlug, string $pageSlug)
196 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
197 $this->checkOwnablePermission(Permission::PageUpdate, $page, $page->getUrl());
199 $editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
200 if ($editorData->getWarnings()) {
201 $this->showWarningNotification(implode("\n", $editorData->getWarnings()));
204 $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
206 return view('pages.edit', $editorData->getViewData());
210 * Update the specified page in storage.
212 * @throws ValidationException
213 * @throws NotFoundException
215 public function update(Request $request, string $bookSlug, string $pageSlug)
217 $this->validate($request, [
218 'name' => ['required', 'string', 'max:255'],
220 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
221 $this->checkOwnablePermission(Permission::PageUpdate, $page);
223 $this->pageRepo->update($page, $request->all());
225 return redirect($page->getUrl());
229 * Save a draft update as a revision.
231 * @throws NotFoundException
233 public function saveDraft(Request $request, int $pageId)
235 $page = $this->queries->findVisibleByIdOrFail($pageId);
236 $this->checkOwnablePermission(Permission::PageUpdate, $page);
238 if (!$this->isSignedIn()) {
239 return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
242 $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
243 $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
245 return response()->json([
246 'status' => 'success',
247 'message' => trans('entities.pages_edit_draft_save_at'),
248 'warning' => implode("\n", $warnings),
249 'timestamp' => $draft->updated_at->timestamp,
254 * Redirect from a special link url which uses the page id rather than the name.
256 * @throws NotFoundException
258 public function redirectFromLink(int $pageId)
260 $page = $this->queries->findVisibleByIdOrFail($pageId);
262 return redirect($page->getUrl());
266 * Show the deletion page for the specified page.
268 * @throws NotFoundException
270 public function showDelete(string $bookSlug, string $pageSlug)
272 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
273 $this->checkOwnablePermission(Permission::PageDelete, $page);
274 $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
276 $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
277 $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
279 return view('pages.delete', [
280 'book' => $page->book,
283 'usedAsTemplate' => $usedAsTemplate,
288 * Show the deletion page for the specified page.
290 * @throws NotFoundException
292 public function showDeleteDraft(string $bookSlug, int $pageId)
294 $page = $this->queries->findVisibleByIdOrFail($pageId);
295 $this->checkOwnablePermission(Permission::PageUpdate, $page);
296 $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
298 $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
299 $this->entityQueries->chapters->start()->where('default_template_id', '=', $page->id)->count() > 0;
301 return view('pages.delete', [
302 'book' => $page->book,
305 'usedAsTemplate' => $usedAsTemplate,
310 * Remove the specified page from storage.
312 * @throws NotFoundException
315 public function destroy(string $bookSlug, string $pageSlug)
317 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
318 $this->checkOwnablePermission(Permission::PageDelete, $page);
319 $parent = $page->getParent();
321 $this->pageRepo->destroy($page);
323 return redirect($parent->getUrl());
327 * Remove the specified draft page from storage.
329 * @throws NotFoundException
332 public function destroyDraft(string $bookSlug, int $pageId)
334 $page = $this->queries->findVisibleByIdOrFail($pageId);
336 $chapter = $page->chapter;
337 $this->checkOwnablePermission(Permission::PageUpdate, $page);
339 $this->pageRepo->destroy($page);
341 $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
343 if ($chapter && userCan(Permission::ChapterView, $chapter)) {
344 return redirect($chapter->getUrl());
347 return redirect($book->getUrl());
351 * Show a listing of recently created pages.
353 public function showRecentlyUpdated()
355 $visibleBelongsScope = function (BelongsTo $query) {
356 $query->scopes('visible');
359 $pages = $this->queries->visibleForList()
360 ->addSelect('updated_by')
361 ->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
362 ->orderBy('updated_at', 'desc')
364 ->setPath(url('/pages/recently-updated'));
366 $this->setPageTitle(trans('entities.recently_updated_pages'));
368 return view('common.detailed-listing-paginated', [
369 'title' => trans('entities.recently_updated_pages'),
370 'entities' => $pages,
371 'showUpdatedBy' => true,
377 * Show the view to choose a new parent to move a page into.
379 * @throws NotFoundException
381 public function showMove(string $bookSlug, string $pageSlug)
383 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
384 $this->checkOwnablePermission(Permission::PageUpdate, $page);
385 $this->checkOwnablePermission(Permission::PageDelete, $page);
387 return view('pages.move', [
388 'book' => $page->book,
394 * Does the action of moving the location of a page.
396 * @throws NotFoundException
399 public function move(Request $request, string $bookSlug, string $pageSlug)
401 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
402 $this->checkOwnablePermission(Permission::PageUpdate, $page);
403 $this->checkOwnablePermission(Permission::PageDelete, $page);
405 $entitySelection = $request->get('entity_selection', null);
406 if ($entitySelection === null || $entitySelection === '') {
407 return redirect($page->getUrl());
411 $this->pageRepo->move($page, $entitySelection);
412 } catch (PermissionsException $exception) {
413 $this->showPermissionError();
414 } catch (Exception $exception) {
415 $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
417 return redirect($page->getUrl('/move'));
420 return redirect($page->getUrl());
424 * Show the view to copy a page.
426 * @throws NotFoundException
428 public function showCopy(string $bookSlug, string $pageSlug)
430 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
431 session()->flashInput(['name' => $page->name]);
433 return view('pages.copy', [
434 'book' => $page->book,
440 * Create a copy of a page within the requested target destination.
442 * @throws NotFoundException
445 public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
447 $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
448 $this->checkOwnablePermission(Permission::PageView, $page);
450 $entitySelection = $request->get('entity_selection') ?: null;
451 $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
453 if (!$newParent instanceof Book && !$newParent instanceof Chapter) {
454 $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
456 return redirect($page->getUrl('/copy'));
459 $this->checkOwnablePermission(Permission::PageCreate, $newParent);
461 $newName = $request->get('name') ?: $page->name;
462 $pageCopy = $cloner->clonePage($page, $newParent, $newName);
463 $this->showSuccessNotification(trans('entities.pages_copy_success'));
465 return redirect($pageCopy->getUrl());