X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/b4dec2a99cbf01dc77ed9c6ec5041a9d8214b332..refs/pull/139/head:/app/Repos/PageRepo.php diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 0c6f4a256..de050e1c7 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -1,59 +1,57 @@ page = $page; $this->pageRevision = $pageRevision; + $this->tagRepo = $tagRepo; + parent::__construct(); } /** - * Check if a page id exists. - * @param $id - * @return bool + * Base query for getting pages, Takes restrictions into account. + * @param bool $allowDrafts + * @return mixed */ - public function idExists($id) + private function pageQuery($allowDrafts = false) { - return $this->page->where('page_id', '=', $id)->count() > 0; + $query = $this->permissionService->enforcePageRestrictions($this->page, 'view'); + if (!$allowDrafts) { + $query = $query->where('draft', '=', false); + } + return $query; } /** * Get a page via a specific ID. * @param $id + * @param bool $allowDrafts * @return mixed */ - public function getById($id) - { - return $this->page->findOrFail($id); - } - - /** - * Get all pages. - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public function getAll() + public function getById($id, $allowDrafts = false) { - return $this->page->all(); + return $this->pageQuery($allowDrafts)->findOrFail($id); } /** @@ -61,15 +59,37 @@ class PageRepo * @param $slug * @param $bookId * @return mixed + * @throws NotFoundException */ public function getBySlug($slug, $bookId) { - $page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); - if ($page === null) abort(404); + $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); + if ($page === null) throw new NotFoundException('Page not found'); return $page; } /** + * Search through page revisions and retrieve + * the last page in the current book that + * has a slug equal to the one given. + * @param $pageSlug + * @param $bookSlug + * @return null | Page + */ + public function findPageUsingOldSlug($pageSlug, $bookSlug) + { + $revision = $this->pageRevision->where('slug', '=', $pageSlug) + ->whereHas('page', function ($query) { + $this->permissionService->enforcePageRestrictions($query); + }) + ->where('type', '=', 'version') + ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') + ->with('page')->first(); + return $revision !== null ? $revision->page : null; + } + + /** + * Get a new Page instance from the given input. * @param $input * @return Page */ @@ -94,8 +114,8 @@ class PageRepo * Save a new page into the system. * Input validation must be done beforehand. * @param array $input - * @param Book $book - * @param int $chapterId + * @param Book $book + * @param int $chapterId * @return Page */ public function saveNew(array $input, Book $book, $chapterId = null) @@ -114,6 +134,53 @@ class PageRepo return $page; } + + /** + * Publish a draft page to make it a normal page. + * Sets the slug and updates the content. + * @param Page $draftPage + * @param array $input + * @return Page + */ + public function publishDraft(Page $draftPage, array $input) + { + $draftPage->fill($input); + + // Save page tags if present + if(isset($input['tags'])) { + $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']); + } + + $draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id); + $draftPage->html = $this->formatHtml($input['html']); + $draftPage->text = strip_tags($draftPage->html); + $draftPage->draft = false; + + $draftPage->save(); + return $draftPage; + } + + /** + * Get a new draft page instance. + * @param Book $book + * @param Chapter|bool $chapter + * @return static + */ + public function getDraftPage(Book $book, $chapter = false) + { + $page = $this->page->newInstance(); + $page->name = 'New Page'; + $page->created_by = auth()->user()->id; + $page->updated_by = auth()->user()->id; + $page->draft = true; + + if ($chapter) $page->chapter_id = $chapter->id; + + $book->pages()->save($page); + $this->permissionService->buildJointPermissionsForEntity($page); + return $page; + } + /** * Formats a page's html to be tagged correctly * within the system. @@ -122,9 +189,9 @@ class PageRepo */ protected function formatHtml($htmlText) { - if($htmlText == '') return $htmlText; + if ($htmlText == '') return $htmlText; libxml_use_internal_errors(true); - $doc = new \DOMDocument(); + $doc = new DOMDocument(); $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); $container = $doc->documentElement; @@ -175,14 +242,18 @@ class PageRepo /** * Gets pages by a search term. * Highlights page content for showing in results. - * @param string $term + * @param string $term * @param array $whereTerms + * @param int $count + * @param array $paginationAppends * @return mixed */ - public function getBySearch($term, $whereTerms = []) + public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) { - $terms = explode(' ', $term); - $pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms); + $terms = $this->prepareSearchTerms($term); + $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)); + $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term); + $pages = $pageQuery->paginate($count)->appends($paginationAppends); // Add highlights to page text. $words = join('|', explode(' ', preg_quote(trim($term), '/'))); @@ -219,7 +290,7 @@ class PageRepo */ public function searchForImage($imageString) { - $pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get(); + $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get(); foreach ($pages as $page) { $page->url = $page->getUrl(); $page->html = ''; @@ -230,8 +301,8 @@ class PageRepo /** * Updates a page with any fillable data and saves it into the database. - * @param Page $page - * @param int $book_id + * @param Page $page + * @param int $book_id * @param string $input * @return Page */ @@ -242,13 +313,28 @@ class PageRepo $this->saveRevision($page); } + // Prevent slug being updated if no name change + if ($page->name !== $input['name']) { + $page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id); + } + + // Save page tags if present + if(isset($input['tags'])) { + $this->tagRepo->saveTagsToEntity($page, $input['tags']); + } + // Update with new details + $userId = auth()->user()->id; $page->fill($input); - $page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id); $page->html = $this->formatHtml($input['html']); $page->text = strip_tags($page->html); - $page->updated_by = auth()->user()->id; + if (setting('app-editor') !== 'markdown') $page->markdown = ''; + $page->updated_by = $userId; $page->save(); + + // Remove all update drafts for this user & page. + $this->userUpdateDraftsQuery($page, $userId)->delete(); + return $page; } @@ -279,9 +365,13 @@ class PageRepo public function saveRevision(Page $page) { $revision = $this->pageRevision->fill($page->toArray()); + if (setting('app-editor') !== 'markdown') $revision->markdown = ''; $revision->page_id = $page->id; + $revision->slug = $page->slug; + $revision->book_slug = $page->book->slug; $revision->created_by = auth()->user()->id; $revision->created_at = $page->updated_at; + $revision->type = 'version'; $revision->save(); // Clear old revisions if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { @@ -291,6 +381,155 @@ class PageRepo return $revision; } + /** + * Save a page update draft. + * @param Page $page + * @param array $data + * @return PageRevision + */ + public function saveUpdateDraft(Page $page, $data = []) + { + $userId = auth()->user()->id; + $drafts = $this->userUpdateDraftsQuery($page, $userId)->get(); + + if ($drafts->count() > 0) { + $draft = $drafts->first(); + } else { + $draft = $this->pageRevision->newInstance(); + $draft->page_id = $page->id; + $draft->slug = $page->slug; + $draft->book_slug = $page->book->slug; + $draft->created_by = $userId; + $draft->type = 'update_draft'; + } + + $draft->fill($data); + if (setting('app-editor') !== 'markdown') $draft->markdown = ''; + + $draft->save(); + return $draft; + } + + /** + * Update a draft page. + * @param Page $page + * @param array $data + * @return Page + */ + public function updateDraftPage(Page $page, $data = []) + { + $page->fill($data); + + if (isset($data['html'])) { + $page->text = strip_tags($data['html']); + } + + $page->save(); + return $page; + } + + /** + * The base query for getting user update drafts. + * @param Page $page + * @param $userId + * @return mixed + */ + private function userUpdateDraftsQuery(Page $page, $userId) + { + return $this->pageRevision->where('created_by', '=', $userId) + ->where('type', 'update_draft') + ->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc'); + } + + /** + * Checks whether a user has a draft version of a particular page or not. + * @param Page $page + * @param $userId + * @return bool + */ + public function hasUserGotPageDraft(Page $page, $userId) + { + return $this->userUpdateDraftsQuery($page, $userId)->count() > 0; + } + + /** + * Get the latest updated draft revision for a particular page and user. + * @param Page $page + * @param $userId + * @return mixed + */ + public function getUserPageDraft(Page $page, $userId) + { + return $this->userUpdateDraftsQuery($page, $userId)->first(); + } + + /** + * Get the notification message that informs the user that they are editing a draft page. + * @param PageRevision $draft + * @return string + */ + public function getUserPageDraftMessage(PageRevision $draft) + { + $message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.'; + if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) { + $message .= "\n This page has been updated by since that time. It is recommended that you discard this draft."; + } + return $message; + } + + /** + * Check if a page is being actively editing. + * Checks for edits since last page updated. + * Passing in a minuted range will check for edits + * within the last x minutes. + * @param Page $page + * @param null $minRange + * @return bool + */ + public function isPageEditingActive(Page $page, $minRange = null) + { + $draftSearch = $this->activePageEditingQuery($page, $minRange); + return $draftSearch->count() > 0; + } + + /** + * Get a notification message concerning the editing activity on + * a particular page. + * @param Page $page + * @param null $minRange + * @return string + */ + public function getPageEditingActiveMessage(Page $page, $minRange = null) + { + $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get(); + $userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has'; + $timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes'; + $message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!'; + return sprintf($message, $userMessage, $timeMessage); + } + + /** + * A query to check for active update drafts on a particular page. + * @param Page $page + * @param null $minRange + * @return mixed + */ + private function activePageEditingQuery(Page $page, $minRange = null) + { + $query = $this->pageRevision->where('type', '=', 'update_draft') + ->where('page_id', '=', $page->id) + ->where('updated_at', '>', $page->updated_at) + ->where('created_by', '!=', auth()->user()->id) + ->with('createdBy'); + + if ($minRange !== null) { + $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange)); + } + + return $query; + } + /** * Gets a single revision via it's id. * @param $id @@ -318,7 +557,7 @@ class PageRepo /** * Changes the related book for the specified page. * Changes the book id of any relations to the page that store the book id. - * @param int $bookId + * @param int $bookId * @param Page $page * @return Page */ @@ -334,6 +573,22 @@ class PageRepo return $page; } + + /** + * Change the page's parent to the given entity. + * @param Page $page + * @param Entity $parent + */ + public function changePageParent(Page $page, Entity $parent) + { + $book = $parent->isA('book') ? $parent : $parent->book; + $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0; + $page->save(); + $page = $this->changeBook($book->id, $page); + $page->load('book'); + $this->permissionService->buildJointPermissionsForEntity($book); + } + /** * Gets a suitable slug for the resource * @param $name @@ -354,11 +609,14 @@ class PageRepo * Destroy a given page along with its dependencies. * @param $page */ - public function destroy($page) + public function destroy(Page $page) { Activity::removeEntity($page); $page->views()->delete(); + $page->tags()->delete(); $page->revisions()->delete(); + $page->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($page); $page->delete(); } @@ -368,7 +626,7 @@ class PageRepo */ public function getRecentlyCreatedPaginated($count = 20) { - return $this->page->orderBy('created_at', 'desc')->paginate($count); + return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count); } /** @@ -377,7 +635,7 @@ class PageRepo */ public function getRecentlyUpdatedPaginated($count = 20) { - return $this->page->orderBy('updated_at', 'desc')->paginate($count); + return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count); } }