diff --git a/app/Chapter.php b/app/Chapter.php index 586ce3fe3..dc23f5ebd 100644 --- a/app/Chapter.php +++ b/app/Chapter.php @@ -18,11 +18,12 @@ class Chapter extends Entity /** * Get the pages that this chapter contains. + * @param string $dir * @return mixed */ - public function pages() + public function pages($dir = 'ASC') { - return $this->hasMany(Page::class)->orderBy('priority', 'ASC'); + return $this->hasMany(Page::class)->orderBy('priority', $dir); } /** diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 715cd2bd8..3c325d0fe 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -3,7 +3,6 @@ use BookStack\Exceptions\FileUploadException; use BookStack\Attachment; use BookStack\Repos\EntityRepo; -use BookStack\Repos\PageRepo; use BookStack\Services\AttachmentService; use Illuminate\Http\Request; @@ -11,21 +10,18 @@ class AttachmentController extends Controller { protected $attachmentService; protected $attachment; - protected $pageRepo; protected $entityRepo; /** * AttachmentController constructor. * @param AttachmentService $attachmentService * @param Attachment $attachment - * @param PageRepo $pageRepo + * @param EntityRepo $entityRepo */ - public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo, PageRepo $pageRepo) + public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo) { $this->attachmentService = $attachmentService; $this->attachment = $attachment; - // TODO - Remove this - $this->pageRepo = $pageRepo; $this->entityRepo = $entityRepo; parent::__construct(); } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index b6856f273..57ac486d5 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -4,10 +4,6 @@ use Activity; use BookStack\Repos\EntityRepo; use BookStack\Repos\UserRepo; use Illuminate\Http\Request; -use BookStack\Http\Requests; -use BookStack\Repos\BookRepo; -use BookStack\Repos\ChapterRepo; -use BookStack\Repos\PageRepo; use Illuminate\Http\Response; use Views; @@ -15,26 +11,16 @@ class BookController extends Controller { protected $entityRepo; - protected $bookRepo; - protected $pageRepo; - protected $chapterRepo; protected $userRepo; /** * BookController constructor. * @param EntityRepo $entityRepo - * @param BookRepo $bookRepo - * @param PageRepo $pageRepo - * @param ChapterRepo $chapterRepo * @param UserRepo $userRepo */ - public function __construct(EntityRepo $entityRepo, BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo) + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo) { $this->entityRepo = $entityRepo; - // TODO - Remove below - $this->bookRepo = $bookRepo; - $this->pageRepo = $pageRepo; - $this->chapterRepo = $chapterRepo; $this->userRepo = $userRepo; parent::__construct(); } @@ -76,7 +62,7 @@ class BookController extends Controller 'name' => 'required|string|max:255', 'description' => 'string|max:1000' ]); - $book = $this->bookRepo->createFromInput($request->all()); + $book = $this->entityRepo->createFromInput('book', $request->all()); Activity::add($book, 'book_create', $book->id); return redirect($book->getUrl()); } @@ -90,7 +76,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $slug); $this->checkOwnablePermission('book-view', $book); - $bookChildren = $this->bookRepo->getChildren($book); + $bookChildren = $this->entityRepo->getBookChildren($book); Views::add($book); $this->setPageTitle($book->getShortName()); return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]); @@ -123,7 +109,7 @@ class BookController extends Controller 'name' => 'required|string|max:255', 'description' => 'string|max:1000' ]); - $book = $this->bookRepo->updateFromInput($book, $request->all()); + $book = $this->entityRepo->updateFromInput('book', $book, $request->all()); Activity::add($book, 'book_update', $book->id); return redirect($book->getUrl()); } @@ -150,7 +136,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-update', $book); - $bookChildren = $this->bookRepo->getChildren($book, true); + $bookChildren = $this->entityRepo->getBookChildren($book, true); $books = $this->entityRepo->getAll('book', false); $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()])); return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); @@ -165,7 +151,7 @@ class BookController extends Controller public function getSortItem($bookSlug) { $book = $this->entityRepo->getBySlug('book', $bookSlug); - $bookChildren = $this->bookRepo->getChildren($book); + $bookChildren = $this->entityRepo->getBookChildren($book); return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); } @@ -202,7 +188,7 @@ class BookController extends Controller // Update models only if there's a change in parent chain or ordering. if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) { - $isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model); + $this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model); $model->priority = $priority; if ($isPage) $model->chapter_id = $chapterId; $model->save(); @@ -222,7 +208,7 @@ class BookController extends Controller } // Update permissions on changed models - $this->bookRepo->buildJointPermissions($updatedModels); + $this->entityRepo->buildJointPermissions($updatedModels); return redirect($book->getUrl()); } @@ -237,8 +223,7 @@ class BookController extends Controller $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-delete', $book); Activity::addMessage('book_delete', 0, $book->name); - Activity::removeEntity($book); - $this->bookRepo->destroy($book); + $this->entityRepo->destroyBook($book); return redirect('/books'); } @@ -269,7 +254,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('restrictions-manage', $book); - $this->bookRepo->updateEntityPermissionsFromRequest($request, $book); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $book); session()->flash('success', trans('entities.books_permissions_updated')); return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index d239b08cc..1760ee5c6 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -4,32 +4,23 @@ use Activity; use BookStack\Repos\EntityRepo; use BookStack\Repos\UserRepo; use Illuminate\Http\Request; -use BookStack\Repos\BookRepo; -use BookStack\Repos\ChapterRepo; use Illuminate\Http\Response; use Views; class ChapterController extends Controller { - protected $bookRepo; - protected $chapterRepo; protected $userRepo; protected $entityRepo; /** * ChapterController constructor. * @param EntityRepo $entityRepo - * @param BookRepo $bookRepo - * @param ChapterRepo $chapterRepo * @param UserRepo $userRepo */ - public function __construct(EntityRepo $entityRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo) + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo) { $this->entityRepo = $entityRepo; - // TODO - Remove below - $this->bookRepo = $bookRepo; - $this->chapterRepo = $chapterRepo; $this->userRepo = $userRepo; parent::__construct(); } @@ -63,8 +54,8 @@ class ChapterController extends Controller $this->checkOwnablePermission('chapter-create', $book); $input = $request->all(); - $input['priority'] = $this->bookRepo->getNewPriority($book); - $chapter = $this->chapterRepo->createFromInput($input, $book); + $input['priority'] = $this->entityRepo->getNewBookPriority($book); + $chapter = $this->entityRepo->createFromInput('chapter', $input, $book); Activity::add($chapter, 'chapter_create', $book->id); return redirect($chapter->getUrl()); } @@ -79,10 +70,10 @@ class ChapterController extends Controller { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-view', $chapter); - $sidebarTree = $this->bookRepo->getChildren($chapter->book); + $sidebarTree = $this->entityRepo->getBookChildren($chapter->book); Views::add($chapter); $this->setPageTitle($chapter->getShortName()); - $pages = $this->chapterRepo->getChildren($chapter); + $pages = $this->entityRepo->getChapterChildren($chapter); return view('chapters/show', [ 'book' => $chapter->book, 'chapter' => $chapter, @@ -153,7 +144,7 @@ class ChapterController extends Controller $book = $chapter->book; $this->checkOwnablePermission('chapter-delete', $chapter); Activity::addMessage('chapter_delete', $book->id, $chapter->name); - $this->chapterRepo->destroy($chapter); + $this->entityRepo->destroyChapter($chapter); return redirect($book->getUrl()); } @@ -206,7 +197,7 @@ class ChapterController extends Controller return redirect()->back(); } - $this->chapterRepo->changeBook($parent->id, $chapter, true); + $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true); Activity::add($chapter, 'chapter_move', $chapter->book->id); session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name])); @@ -241,7 +232,7 @@ class ChapterController extends Controller { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $chapter); - $this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter); session()->flash('success', trans('entities.chapters_permissions_success')); return redirect($chapter->getUrl()); } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index f073bea0a..77c320e07 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -1,6 +1,7 @@ imageRepo->getById($id); $this->checkOwnablePermission('image-delete', $image); @@ -163,7 +164,7 @@ class ImageController extends Controller // Check if this image is used on any pages $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); if (!$isForced) { - $pageSearch = $pageRepo->searchForImage($image->url); + $pageSearch = $entityRepo->searchForImage($image->url); if ($pageSearch !== false) { return response()->json($pageSearch, 400); } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 5a33ecb37..6ed9fc30c 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -7,9 +7,6 @@ use BookStack\Repos\UserRepo; use BookStack\Services\ExportService; use Carbon\Carbon; use Illuminate\Http\Request; -use BookStack\Repos\BookRepo; -use BookStack\Repos\ChapterRepo; -use BookStack\Repos\PageRepo; use Illuminate\Http\Response; use Views; use GatherContent\Htmldiff\Htmldiff; @@ -18,28 +15,18 @@ class PageController extends Controller { protected $entityRepo; - protected $pageRepo; - protected $bookRepo; - protected $chapterRepo; protected $exportService; protected $userRepo; /** * PageController constructor. * @param EntityRepo $entityRepo - * @param PageRepo $pageRepo - * @param BookRepo $bookRepo - * @param ChapterRepo $chapterRepo * @param ExportService $exportService * @param UserRepo $userRepo */ - public function __construct(EntityRepo $entityRepo, PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo) + public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo) { $this->entityRepo = $entityRepo; - // TODO - remove below; - $this->pageRepo = $pageRepo; - $this->bookRepo = $bookRepo; - $this->chapterRepo = $chapterRepo; $this->exportService = $exportService; $this->userRepo = $userRepo; parent::__construct(); @@ -61,7 +48,7 @@ class PageController extends Controller // Redirect to draft edit screen if signed in if ($this->signedIn) { - $draft = $this->pageRepo->getDraftPage($book, $chapter); + $draft = $this->entityRepo->getDraftPage($book, $chapter); return redirect($draft->getUrl()); } @@ -89,8 +76,8 @@ class PageController extends Controller $parent = $chapter ? $chapter : $book; $this->checkOwnablePermission('page-create', $parent); - $page = $this->pageRepo->getDraftPage($book, $chapter); - $this->pageRepo->publishDraft($page, [ + $page = $this->entityRepo->getDraftPage($book, $chapter); + $this->entityRepo->publishPageDraft($page, [ 'name' => $request->get('name'), 'html' => '' ]); @@ -141,12 +128,12 @@ class PageController extends Controller $this->checkOwnablePermission('page-create', $parent); if ($parent->isA('chapter')) { - $input['priority'] = $this->chapterRepo->getNewPriority($parent); + $input['priority'] = $this->entityRepo->getNewChapterPriority($parent); } else { - $input['priority'] = $this->bookRepo->getNewPriority($parent); + $input['priority'] = $this->entityRepo->getNewBookPriority($parent); } - $page = $this->pageRepo->publishDraft($draftPage, $input); + $page = $this->entityRepo->publishPageDraft($draftPage, $input); Activity::add($page, 'page_create', $book->id); return redirect($page->getUrl()); @@ -164,15 +151,15 @@ class PageController extends Controller try { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); } catch (NotFoundException $e) { - $page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug); + $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug); if ($page === null) abort(404); return redirect($page->getUrl()); } $this->checkOwnablePermission('page-view', $page); - $sidebarTree = $this->bookRepo->getChildren($page->book); - $pageNav = $this->pageRepo->getPageNav($page); + $sidebarTree = $this->entityRepo->getBookChildren($page->book); + $pageNav = $this->entityRepo->getPageNav($page); Views::add($page); $this->setPageTitle($page->getShortName()); @@ -206,18 +193,18 @@ class PageController extends Controller // Check for active editing $warnings = []; - if ($this->pageRepo->isPageEditingActive($page, 60)) { - $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60); + if ($this->entityRepo->isPageEditingActive($page, 60)) { + $warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60); } // Check for a current draft version for this user - if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { - $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); + if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { + $draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id); $page->name = $draft->name; $page->html = $draft->html; $page->markdown = $draft->markdown; $page->isDraft = true; - $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft); + $warnings [] = $this->entityRepo->getUserPageDraftMessage($draft); } if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings)); @@ -245,7 +232,7 @@ class PageController extends Controller ]); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $this->pageRepo->updatePage($page, $page->book->id, $request->all()); + $this->entityRepo->updatePage($page, $page->book->id, $request->all()); Activity::add($page, 'page_update', $page->book->id); return redirect($page->getUrl()); } @@ -268,11 +255,7 @@ class PageController extends Controller ], 500); } - if ($page->draft) { - $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown'])); - } else { - $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown'])); - } + $draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown'])); $updateTime = $draft->updated_at->timestamp; $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset; @@ -339,7 +322,7 @@ class PageController extends Controller $this->checkOwnablePermission('page-delete', $page); Activity::addMessage('page_delete', $book->id, $page->name); session()->flash('success', trans('entities.pages_delete_success')); - $this->pageRepo->destroy($page); + $this->entityRepo->destroyPage($page); return redirect($book->getUrl()); } @@ -356,7 +339,7 @@ class PageController extends Controller $book = $page->book; $this->checkOwnablePermission('page-update', $page); session()->flash('success', trans('entities.pages_delete_draft_success')); - $this->pageRepo->destroy($page); + $this->entityRepo->destroyPage($page); return redirect($book->getUrl()); } @@ -383,7 +366,7 @@ class PageController extends Controller public function showRevision($bookSlug, $pageSlug, $revisionId) { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); - $revision = $this->pageRepo->getRevisionById($revisionId); + $revision = $this->entityRepo->getById('page_revision', $revisionId, false); $page->fill($revision->toArray()); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()])); @@ -404,7 +387,7 @@ class PageController extends Controller public function showRevisionChanges($bookSlug, $pageSlug, $revisionId) { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); - $revision = $this->pageRepo->getRevisionById($revisionId); + $revision = $this->entityRepo->getById('page_revision', $revisionId); $prev = $revision->getPrevious(); $prevContent = ($prev === null) ? '' : $prev->html; @@ -431,7 +414,7 @@ class PageController extends Controller { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $page = $this->pageRepo->restoreRevision($page, $page->book, $revisionId); + $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId); Activity::add($page, 'page_restore', $page->book->id); return redirect($page->getUrl()); } @@ -491,7 +474,7 @@ class PageController extends Controller */ public function showRecentlyCreated() { - $pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created')); + $pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created')); return view('pages/detailed-listing', [ 'title' => trans('entities.recently_created_pages'), 'pages' => $pages @@ -504,7 +487,7 @@ class PageController extends Controller */ public function showRecentlyUpdated() { - $pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated')); + $pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated')); return view('pages/detailed-listing', [ 'title' => trans('entities.recently_updated_pages'), 'pages' => $pages @@ -575,7 +558,7 @@ class PageController extends Controller return redirect()->back(); } - $this->pageRepo->changePageParent($page, $parent); + $this->entityRepo->changePageParent($page, $parent); Activity::add($page, 'page_move', $page->book->id); session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name])); @@ -593,7 +576,7 @@ class PageController extends Controller { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $page); - $this->pageRepo->updateEntityPermissionsFromRequest($request, $page); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $page); session()->flash('success', trans('entities.pages_permissions_success')); return redirect($page->getUrl()); } diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php deleted file mode 100644 index f5d19bc37..000000000 --- a/app/Repos/BookRepo.php +++ /dev/null @@ -1,124 +0,0 @@ -pageRepo = $pageRepo; - $this->chapterRepo = $chapterRepo; - parent::__construct(); - } - - /** - * Get a new book instance from request input. - * @param array $input - * @return Book - */ - public function createFromInput($input) - { - $book = $this->book->newInstance($input); - $book->slug = $this->findSuitableSlug('book', $book->name); - $book->created_by = user()->id; - $book->updated_by = user()->id; - $book->save(); - $this->permissionService->buildJointPermissionsForEntity($book); - return $book; - } - - /** - * Update the given book from user input. - * @param Book $book - * @param $input - * @return Book - */ - public function updateFromInput(Book $book, $input) - { - if ($book->name !== $input['name']) { - $book->slug = $this->findSuitableSlug('book', $input['name'], $book->id); - } - $book->fill($input); - $book->updated_by = user()->id; - $book->save(); - $this->permissionService->buildJointPermissionsForEntity($book); - return $book; - } - - /** - * Destroy the given book. - * @param Book $book - * @throws \Exception - */ - public function destroy(Book $book) - { - foreach ($book->pages as $page) { - $this->pageRepo->destroy($page); - } - foreach ($book->chapters as $chapter) { - $this->chapterRepo->destroy($chapter); - } - $book->views()->delete(); - $book->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($book); - $book->delete(); - } - - /** - * Get the next child element priority. - * @param Book $book - * @return int - */ - public function getNewPriority($book) - { - $lastElem = $this->getChildren($book)->pop(); - return $lastElem ? $lastElem->priority + 1 : 0; - } - - /** - * Get all child objects of a book. - * Returns a sorted collection of Pages and Chapters. - * Loads the book slug onto child elements to prevent access database access for getting the slug. - * @param Book $book - * @param bool $filterDrafts - * @return mixed - */ - public function getChildren(Book $book, $filterDrafts = false) - { - $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts); - $entities = []; - $parents = []; - $tree = []; - - foreach ($q as $index => $rawEntity) { - if ($rawEntity->entity_type === 'Bookstack\\Page') { - $entities[$index] = $this->page->newFromBuilder($rawEntity); - } else if ($rawEntity->entity_type === 'Bookstack\\Chapter') { - $entities[$index] = $this->chapter->newFromBuilder($rawEntity); - $key = $entities[$index]->entity_type . ':' . $entities[$index]->id; - $parents[$key] = $entities[$index]; - $parents[$key]->setAttribute('pages', collect()); - } - if ($entities[$index]->chapter_id === 0) $tree[] = $entities[$index]; - $entities[$index]->book = $book; - } - - foreach ($entities as $entity) { - if ($entity->chapter_id === 0) continue; - $parentKey = 'Bookstack\\Chapter:' . $entity->chapter_id; - $chapter = $parents[$parentKey]; - $chapter->pages->push($entity); - } - - return collect($tree); - } - -} \ No newline at end of file diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php deleted file mode 100644 index afbf312da..000000000 --- a/app/Repos/ChapterRepo.php +++ /dev/null @@ -1,119 +0,0 @@ -pageRepo = $pageRepo; - parent::__construct(); - } - - /** - * Get the child items for a chapter - * @param Chapter $chapter - */ - public function getChildren(Chapter $chapter) - { - $pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get(); - // Sort items with drafts first then by priority. - return $pages->sortBy(function ($child, $key) { - $score = $child->priority; - if ($child->draft) $score -= 100; - return $score; - }); - } - - /** - * Create a new chapter from request input. - * @param $input - * @param Book $book - * @return Chapter - */ - public function createFromInput($input, Book $book) - { - $chapter = $this->chapter->newInstance($input); - $chapter->slug = $this->findSuitableSlug('chapter', $chapter->name, false, $book->id); - $chapter->created_by = user()->id; - $chapter->updated_by = user()->id; - $chapter = $book->chapters()->save($chapter); - $this->permissionService->buildJointPermissionsForEntity($chapter); - return $chapter; - } - - /** - * Destroy a chapter and its relations by providing its slug. - * @param Chapter $chapter - */ - public function destroy(Chapter $chapter) - { - if (count($chapter->pages) > 0) { - foreach ($chapter->pages as $page) { - $page->chapter_id = 0; - $page->save(); - } - } - Activity::removeEntity($chapter); - $chapter->views()->delete(); - $chapter->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($chapter); - $chapter->delete(); - } - - - /** - * Get a new priority value for a new page to be added - * to the given chapter. - * @param Chapter $chapter - * @return int - */ - public function getNewPriority(Chapter $chapter) - { - $lastPage = $chapter->pages->last(); - return $lastPage !== null ? $lastPage->priority + 1 : 0; - } - - /** - * Changes the book relation of this chapter. - * @param $bookId - * @param Chapter $chapter - * @param bool $rebuildPermissions - * @return Chapter - */ - public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false) - { - $chapter->book_id = $bookId; - // Update related activity - foreach ($chapter->activity as $activity) { - $activity->book_id = $bookId; - $activity->save(); - } - $chapter->slug = $this->findSuitableSlug('chapter', $chapter->name, $chapter->id, $bookId); - $chapter->save(); - // Update all child pages - foreach ($chapter->pages as $page) { - $this->pageRepo->changeBook($bookId, $page); - } - - // Update permissions if applicable - if ($rebuildPermissions) { - $chapter->load('book'); - $this->permissionService->buildJointPermissionsForEntity($chapter->book); - } - - return $chapter; - } - -} \ No newline at end of file diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index dd3016c6c..95666a66a 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -5,8 +5,13 @@ use BookStack\Chapter; use BookStack\Entity; use BookStack\Exceptions\NotFoundException; use BookStack\Page; +use BookStack\PageRevision; +use BookStack\Services\AttachmentService; use BookStack\Services\PermissionService; use BookStack\Services\ViewService; +use Carbon\Carbon; +use DOMDocument; +use DOMXPath; use Illuminate\Support\Collection; class EntityRepo @@ -27,6 +32,11 @@ class EntityRepo */ public $page; + /** + * @var PageRevision + */ + protected $pageRevision; + /** * Base entity instances keyed by type * @var []Entity @@ -43,6 +53,11 @@ class EntityRepo */ protected $viewService; + /** + * @var TagRepo + */ + protected $tagRepo; + /** * Acceptable operators to be used in a query * @var array @@ -51,20 +66,32 @@ class EntityRepo /** * EntityService constructor. + * @param Book $book + * @param Chapter $chapter + * @param Page $page + * @param PageRevision $pageRevision + * @param ViewService $viewService + * @param PermissionService $permissionService + * @param TagRepo $tagRepo */ - public function __construct() + public function __construct( + Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision, + ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo + ) { - // TODO - Redo this to come via injection - $this->book = app(Book::class); - $this->chapter = app(Chapter::class); - $this->page = app(Page::class); + $this->book = $book; + $this->chapter = $chapter; + $this->page = $page; + $this->pageRevision = $pageRevision; $this->entities = [ 'page' => $this->page, 'chapter' => $this->chapter, - 'book' => $this->book + 'book' => $this->book, + 'page_revision' => $this->pageRevision ]; - $this->viewService = app(ViewService::class); - $this->permissionService = app(PermissionService::class); + $this->viewService = $viewService; + $this->permissionService = $permissionService; + $this->tagRepo = $tagRepo; } /** @@ -139,6 +166,27 @@ class EntityRepo return $entity; } + + /** + * Search through page revisions and retrieve the last page in the + * current book that has a slug equal to the one given. + * @param string $pageSlug + * @param string $bookSlug + * @return null|Page + */ + public function getPageByOldSlug($pageSlug, $bookSlug) + { + $revision = $this->pageRevision->where('slug', '=', $pageSlug) + ->whereHas('page', function ($query) { + $this->permissionService->enforceEntityRestrictions('page', $query); + }) + ->where('type', '=', 'version') + ->where('book_slug', '=', $bookSlug) + ->orderBy('created_at', 'desc') + ->with('page')->first(); + return $revision !== null ? $revision->page : null; + } + /** * Get all entities of a type limited by count unless count if false. * @param string $type @@ -212,6 +260,28 @@ class EntityRepo return $this->viewService->getUserRecentlyViewed($count, $page, $filter); } + /** + * Get the latest pages added to the system with pagination. + * @param string $type + * @param int $count + * @return mixed + */ + public function getRecentlyCreatedPaginated($type, $count = 20) + { + return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count); + } + + /** + * Get the latest pages added to the system with pagination. + * @param string $type + * @param int $count + * @return mixed + */ + public function getRecentlyUpdatedPaginated($type, $count = 20) + { + return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count); + } + /** * Get the most popular entities base on all views. * @param string|bool $type @@ -238,10 +308,68 @@ class EntityRepo ->skip($count * $page)->take($count)->get(); } + /** + * Get all child objects of a book. + * Returns a sorted collection of Pages and Chapters. + * Loads the book slug onto child elements to prevent access database access for getting the slug. + * @param Book $book + * @param bool $filterDrafts + * @return mixed + */ + public function getBookChildren(Book $book, $filterDrafts = false) + { + $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts); + $entities = []; + $parents = []; + $tree = []; + + foreach ($q as $index => $rawEntity) { + if ($rawEntity->entity_type === 'Bookstack\\Page') { + $entities[$index] = $this->page->newFromBuilder($rawEntity); + } else if ($rawEntity->entity_type === 'Bookstack\\Chapter') { + $entities[$index] = $this->chapter->newFromBuilder($rawEntity); + $key = $entities[$index]->entity_type . ':' . $entities[$index]->id; + $parents[$key] = $entities[$index]; + $parents[$key]->setAttribute('pages', collect()); + } + if ($entities[$index]->chapter_id === 0) $tree[] = $entities[$index]; + $entities[$index]->book = $book; + } + + foreach ($entities as $entity) { + if ($entity->chapter_id === 0) continue; + $parentKey = 'Bookstack\\Chapter:' . $entity->chapter_id; + $chapter = $parents[$parentKey]; + $chapter->pages->push($entity); + } + + return collect($tree); + } + + /** + * Get the child items for a chapter sorted by priority but + * with draft items floated to the top. + * @param Chapter $chapter + */ + public function getChapterChildren(Chapter $chapter) + { + return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages()) + ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get(); + } + + /** + * Search entities of a type via a given query. + * @param string $type + * @param string $term + * @param array $whereTerms + * @param int $count + * @param array $paginationAppends + * @return mixed + */ public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = []) { $terms = $this->prepareSearchTerms($term); - $q = $this->permissionService->enforceChapterRestrictions($this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms)); + $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms)); $q = $this->addAdvancedSearchQueries($q, $term); $entities = $q->paginate($count)->appends($paginationAppends); $words = join('|', explode(' ', preg_quote(trim($term), '/'))); @@ -280,6 +408,28 @@ class EntityRepo return $entities; } + /** + * Get the next sequential priority for a new child element in the given book. + * @param Book $book + * @return int + */ + public function getNewBookPriority(Book $book) + { + $lastElem = $this->getBookChildren($book)->pop(); + return $lastElem ? $lastElem->priority + 1 : 0; + } + + /** + * Get a new priority for a new page to be added to the given chapter. + * @param Chapter $chapter + * @return int + */ + public function getNewChapterPriority(Chapter $chapter) + { + $lastPage = $chapter->pages('DESC')->first(); + return $lastPage !== null ? $lastPage->priority + 1 : 0; + } + /** * Find a suitable slug for an entity. * @param string $type @@ -437,6 +587,81 @@ class EntityRepo return $query; } + /** + * Create a new entity from request input. + * Used for books and chapters. + * @param string $type + * @param array $input + * @param bool|Book $book + * @return Entity + */ + public function createFromInput($type, $input = [], $book = false) + { + $isChapter = strtolower($type) === 'chapter'; + $entity = $this->getEntity($type)->newInstance($input); + $entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false); + $entity->created_by = user()->id; + $entity->updated_by = user()->id; + $isChapter ? $book->chapters()->save($entity) : $entity->save(); + $this->permissionService->buildJointPermissionsForEntity($entity); + return $entity; + } + + /** + * Update entity details from request input. + * Use for books and chapters + * @param string $type + * @param Entity $entityModel + * @param array $input + * @return Entity + */ + public function updateFromInput($type, Entity $entityModel, $input = []) + { + if ($entityModel->name !== $input['name']) { + $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id); + } + $entityModel->fill($input); + $entityModel->updated_by = user()->id; + $entityModel->save(); + $this->permissionService->buildJointPermissionsForEntity($entityModel); + return $entityModel; + } + + /** + * Change the book that an entity belongs to. + * @param string $type + * @param integer $newBookId + * @param Entity $entity + * @param bool $rebuildPermissions + * @return Entity + */ + public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false) + { + $entity->book_id = $newBookId; + // Update related activity + foreach ($entity->activity as $activity) { + $activity->book_id = $newBookId; + $activity->save(); + } + $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId); + $entity->save(); + + // Update all child pages if a chapter + if (strtolower($type) === 'chapter') { + foreach ($entity->pages as $page) { + $this->changeBook('page', $newBookId, $page, false); + } + } + + // Update permissions if applicable + if ($rebuildPermissions) { + $entity->load('book'); + $this->permissionService->buildJointPermissionsForEntity($entity->book); + } + + return $entity; + } + /** * Alias method to update the book jointPermissions in the PermissionService. * @param Collection $collection collection on entities @@ -459,6 +684,463 @@ class EntityRepo return $slug; } + /** + * 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 publishPageDraft(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('page', $draftPage->name, false, $draftPage->book->id); + $draftPage->html = $this->formatHtml($input['html']); + $draftPage->text = strip_tags($draftPage->html); + $draftPage->draft = false; + + $draftPage->save(); + $this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); + + return $draftPage; + } + + /** + * Saves a page revision into the system. + * @param Page $page + * @param null|string $summary + * @return PageRevision + */ + public function savePageRevision(Page $page, $summary = null) + { + $revision = $this->pageRevision->newInstance($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 = user()->id; + $revision->created_at = $page->updated_at; + $revision->type = 'version'; + $revision->summary = $summary; + $revision->save(); + + // Clear old revisions + if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { + $this->pageRevision->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); + } + + return $revision; + } + + /** + * Formats a page's html to be tagged correctly + * within the system. + * @param string $htmlText + * @return string + */ + protected function formatHtml($htmlText) + { + if ($htmlText == '') return $htmlText; + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); + + $container = $doc->documentElement; + $body = $container->childNodes->item(0); + $childNodes = $body->childNodes; + + // Ensure no duplicate ids are used + $idArray = []; + + foreach ($childNodes as $index => $childNode) { + /** @var \DOMElement $childNode */ + if (get_class($childNode) !== 'DOMElement') continue; + + // Overwrite id if not a BookStack custom id + if ($childNode->hasAttribute('id')) { + $id = $childNode->getAttribute('id'); + if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { + $idArray[] = $id; + continue; + }; + } + + // Create an unique id for the element + // Uses the content as a basis to ensure output is the same every time + // the same content is passed through. + $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20); + $newId = urlencode($contentId); + $loopIndex = 0; + while (in_array($newId, $idArray)) { + $newId = urlencode($contentId . '-' . $loopIndex); + $loopIndex++; + } + + $childNode->setAttribute('id', $newId); + $idArray[] = $newId; + } + + // Generate inner html as a string + $html = ''; + foreach ($childNodes as $childNode) { + $html .= $doc->saveHTML($childNode); + } + + return $html; + } + + /** + * Get a new draft page instance. + * @param Book $book + * @param Chapter|bool $chapter + * @return Page + */ + public function getDraftPage(Book $book, $chapter = false) + { + $page = $this->page->newInstance(); + $page->name = trans('entities.pages_initial_name'); + $page->created_by = user()->id; + $page->updated_by = user()->id; + $page->draft = true; + + if ($chapter) $page->chapter_id = $chapter->id; + + $book->pages()->save($page); + $this->permissionService->buildJointPermissionsForEntity($page); + return $page; + } + + /** + * Search for image usage within page content. + * @param $imageString + * @return mixed + */ + public function searchForImage($imageString) + { + $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(); + foreach ($pages as $page) { + $page->url = $page->getUrl(); + $page->html = ''; + $page->text = ''; + } + return count($pages) > 0 ? $pages : false; + } + + /** + * Parse the headers on the page to get a navigation menu + * @param Page $page + * @return array + */ + public function getPageNav(Page $page) + { + if ($page->html == '') return null; + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8')); + $xPath = new DOMXPath($doc); + $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); + + if (is_null($headers)) return null; + + $tree = []; + foreach ($headers as $header) { + $text = $header->nodeValue; + $tree[] = [ + 'nodeName' => strtolower($header->nodeName), + 'level' => intval(str_replace('h', '', $header->nodeName)), + 'link' => '#' . $header->getAttribute('id'), + 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text + ]; + } + return $tree; + } + + /** + * Updates a page with any fillable data and saves it into the database. + * @param Page $page + * @param int $book_id + * @param array $input + * @return Page + */ + public function updatePage(Page $page, $book_id, $input) + { + // Hold the old details to compare later + $oldHtml = $page->html; + $oldName = $page->name; + + // Prevent slug being updated if no name change + if ($page->name !== $input['name']) { + $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id); + } + + // Save page tags if present + if (isset($input['tags'])) { + $this->tagRepo->saveTagsToEntity($page, $input['tags']); + } + + // Update with new details + $userId = user()->id; + $page->fill($input); + $page->html = $this->formatHtml($input['html']); + $page->text = strip_tags($page->html); + if (setting('app-editor') !== 'markdown') $page->markdown = ''; + $page->updated_by = $userId; + $page->save(); + + // Remove all update drafts for this user & page. + $this->userUpdatePageDraftsQuery($page, $userId)->delete(); + + // Save a revision after updating + if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { + $this->savePageRevision($page, $input['summary']); + } + + return $page; + } + + /** + * The base query for getting user update drafts. + * @param Page $page + * @param $userId + * @return mixed + */ + protected function userUpdatePageDraftsQuery(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->userUpdatePageDraftsQuery($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->userUpdatePageDraftsQuery($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 = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]); + if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message; + return $message . "\n" . trans('entities.pages_draft_edited_notification'); + } + + /** + * 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; + } + + /** + * A query to check for active update drafts on a particular page. + * @param Page $page + * @param null $minRange + * @return mixed + */ + protected 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', '!=', user()->id) + ->with('createdBy'); + + if ($minRange !== null) { + $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange)); + } + + return $query; + } + + /** + * Restores a revision's content back into a page. + * @param Page $page + * @param Book $book + * @param int $revisionId + * @return Page + */ + public function restorePageRevision(Page $page, Book $book, $revisionId) + { + $this->savePageRevision($page); + $revision = $this->getById('page_revision', $revisionId); + $page->fill($revision->toArray()); + $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id); + $page->text = strip_tags($page->html); + $page->updated_by = user()->id; + $page->save(); + return $page; + } + + + /** + * Save a page update draft. + * @param Page $page + * @param array $data + * @return PageRevision|Page + */ + public function updatePageDraft(Page $page, $data = []) + { + // If the page itself is a draft simply update that + if ($page->draft) { + $page->fill($data); + if (isset($data['html'])) { + $page->text = strip_tags($data['html']); + } + $page->save(); + return $page; + } + + // Otherwise save the data to a revision + $userId = user()->id; + $drafts = $this->userUpdatePageDraftsQuery($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; + } + + /** + * 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 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]); + $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]); + return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]); + } + + /** + * 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(); + if ($page->book->id !== $book->id) { + $page = $this->changeBook('page', $book->id, $page); + } + $page->load('book'); + $this->permissionService->buildJointPermissionsForEntity($book); + } + + /** + * Destroy the provided book and all its child entities. + * @param Book $book + */ + public function destroyBook(Book $book) + { + foreach ($book->pages as $page) { + $this->destroyPage($page); + } + foreach ($book->chapters as $chapter) { + $this->destroyChapter($chapter); + } + \Activity::removeEntity($book); + $book->views()->delete(); + $book->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($book); + $book->delete(); + } + + /** + * Destroy a chapter and its relations. + * @param Chapter $chapter + */ + public function destroyChapter(Chapter $chapter) + { + if (count($chapter->pages) > 0) { + foreach ($chapter->pages as $page) { + $page->chapter_id = 0; + $page->save(); + } + } + \Activity::removeEntity($chapter); + $chapter->views()->delete(); + $chapter->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($chapter); + $chapter->delete(); + } + + /** + * Destroy a given page along with its dependencies. + * @param Page $page + */ + public function destroyPage(Page $page) + { + \Activity::removeEntity($page); + $page->views()->delete(); + $page->tags()->delete(); + $page->revisions()->delete(); + $page->permissions()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($page); + + // Delete Attached Files + $attachmentService = app(AttachmentService::class); + foreach ($page->attachments as $attachment) { + $attachmentService->deleteFile($attachment); + } + + $page->delete(); + } + } diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php deleted file mode 100644 index 699e6ecc5..000000000 --- a/app/Repos/PageRepo.php +++ /dev/null @@ -1,554 +0,0 @@ -pageRevision = $pageRevision; - $this->tagRepo = $tagRepo; - parent::__construct(); - } - - /** - * Base query for getting pages, Takes restrictions into account. - * @param bool $allowDrafts - * @return mixed - */ - private function pageQuery($allowDrafts = false) - { - $query = $this->permissionService->enforcePageRestrictions($this->page, 'view'); - if (!$allowDrafts) { - $query = $query->where('draft', '=', false); - } - return $query; - } - - /** - * 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; - } - - /** - * Count the pages with a particular slug within a book. - * @param $slug - * @param $bookId - * @return mixed - */ - public function countBySlug($slug, $bookId) - { - return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count(); - } - - /** - * 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('page', $draftPage->name, false, $draftPage->book->id); - $draftPage->html = $this->formatHtml($input['html']); - $draftPage->text = strip_tags($draftPage->html); - $draftPage->draft = false; - - $draftPage->save(); - $this->saveRevision($draftPage, trans('entities.pages_initial_revision')); - - return $draftPage; - } - - /** - * Get a new draft page instance. - * @param Book $book - * @param Chapter|bool $chapter - * @return Page - */ - public function getDraftPage(Book $book, $chapter = false) - { - $page = $this->page->newInstance(); - $page->name = trans('entities.pages_initial_name'); - $page->created_by = user()->id; - $page->updated_by = user()->id; - $page->draft = true; - - if ($chapter) $page->chapter_id = $chapter->id; - - $book->pages()->save($page); - $this->permissionService->buildJointPermissionsForEntity($page); - return $page; - } - - /** - * Parse te headers on the page to get a navigation menu - * @param Page $page - * @return array - */ - public function getPageNav(Page $page) - { - if ($page->html == '') return null; - libxml_use_internal_errors(true); - $doc = new DOMDocument(); - $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8')); - $xPath = new DOMXPath($doc); - $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); - - if (is_null($headers)) return null; - - $tree = []; - foreach ($headers as $header) { - $text = $header->nodeValue; - $tree[] = [ - 'nodeName' => strtolower($header->nodeName), - 'level' => intval(str_replace('h', '', $header->nodeName)), - 'link' => '#' . $header->getAttribute('id'), - 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text - ]; - } - return $tree; - } - - /** - * Formats a page's html to be tagged correctly - * within the system. - * @param string $htmlText - * @return string - */ - protected function formatHtml($htmlText) - { - if ($htmlText == '') return $htmlText; - libxml_use_internal_errors(true); - $doc = new DOMDocument(); - $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); - - $container = $doc->documentElement; - $body = $container->childNodes->item(0); - $childNodes = $body->childNodes; - - // Ensure no duplicate ids are used - $idArray = []; - - foreach ($childNodes as $index => $childNode) { - /** @var \DOMElement $childNode */ - if (get_class($childNode) !== 'DOMElement') continue; - - // Overwrite id if not a BookStack custom id - if ($childNode->hasAttribute('id')) { - $id = $childNode->getAttribute('id'); - if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { - $idArray[] = $id; - continue; - }; - } - - // Create an unique id for the element - // Uses the content as a basis to ensure output is the same every time - // the same content is passed through. - $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20); - $newId = urlencode($contentId); - $loopIndex = 0; - while (in_array($newId, $idArray)) { - $newId = urlencode($contentId . '-' . $loopIndex); - $loopIndex++; - } - - $childNode->setAttribute('id', $newId); - $idArray[] = $newId; - } - - // Generate inner html as a string - $html = ''; - foreach ($childNodes as $childNode) { - $html .= $doc->saveHTML($childNode); - } - - return $html; - } - - - /** - * Search for image usage. - * @param $imageString - * @return mixed - */ - public function searchForImage($imageString) - { - $pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get(); - foreach ($pages as $page) { - $page->url = $page->getUrl(); - $page->html = ''; - $page->text = ''; - } - return count($pages) > 0 ? $pages : false; - } - - /** - * Updates a page with any fillable data and saves it into the database. - * @param Page $page - * @param int $book_id - * @param string $input - * @return Page - */ - public function updatePage(Page $page, $book_id, $input) - { - // Hold the old details to compare later - $oldHtml = $page->html; - $oldName = $page->name; - - // Prevent slug being updated if no name change - if ($page->name !== $input['name']) { - $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id); - } - - // Save page tags if present - if (isset($input['tags'])) { - $this->tagRepo->saveTagsToEntity($page, $input['tags']); - } - - // Update with new details - $userId = user()->id; - $page->fill($input); - $page->html = $this->formatHtml($input['html']); - $page->text = strip_tags($page->html); - 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(); - - // Save a revision after updating - if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { - $this->saveRevision($page, $input['summary']); - } - - return $page; - } - - /** - * Restores a revision's content back into a page. - * @param Page $page - * @param Book $book - * @param int $revisionId - * @return Page - */ - public function restoreRevision(Page $page, Book $book, $revisionId) - { - $this->saveRevision($page); - $revision = $this->getRevisionById($revisionId); - $page->fill($revision->toArray()); - $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id); - $page->text = strip_tags($page->html); - $page->updated_by = user()->id; - $page->save(); - return $page; - } - - /** - * Saves a page revision into the system. - * @param Page $page - * @param null|string $summary - * @return $this - */ - public function saveRevision(Page $page, $summary = null) - { - $revision = $this->pageRevision->newInstance($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 = user()->id; - $revision->created_at = $page->updated_at; - $revision->type = 'version'; - $revision->summary = $summary; - $revision->save(); - - // Clear old revisions - if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { - $this->pageRevision->where('page_id', '=', $page->id) - ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); - } - - return $revision; - } - - /** - * Save a page update draft. - * @param Page $page - * @param array $data - * @return PageRevision - */ - public function saveUpdateDraft(Page $page, $data = []) - { - $userId = 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 = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]); - if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message; - return $message . "\n" . trans('entities.pages_draft_edited_notification'); - } - - /** - * 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 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]); - $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]); - return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $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', '!=', 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 - * @return PageRevision - */ - public function getRevisionById($id) - { - return $this->pageRevision->findOrFail($id); - } - - /** - * 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 Page $page - * @return Page - */ - public function changeBook($bookId, Page $page) - { - $page->book_id = $bookId; - foreach ($page->activity as $activity) { - $activity->book_id = $bookId; - $activity->save(); - } - $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $bookId); - $page->save(); - 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); - } - - /** - * Destroy a given page along with its dependencies. - * @param $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); - - // Delete AttachedFiles - $attachmentService = app(AttachmentService::class); - foreach ($page->attachments as $attachment) { - $attachmentService->deleteFile($attachment); - } - - $page->delete(); - } - - /** - * Get the latest pages added to the system. - * @param $count - * @return mixed - */ - public function getRecentlyCreatedPaginated($count = 20) - { - return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count); - } - - /** - * Get the latest pages added to the system. - * @param $count - * @return mixed - */ - public function getRecentlyUpdatedPaginated($count = 20) - { - return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count); - } - -} diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index 6e422c4f4..c6350db1a 100644 --- a/app/Repos/TagRepo.php +++ b/app/Repos/TagRepo.php @@ -38,7 +38,7 @@ class TagRepo { $entityInstance = $this->entity->getEntityInstance($entityType); $searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags'); - $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action); + $searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action); return $searchQuery->first(); } diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 0db2d1b5e..467bf95da 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -516,42 +516,6 @@ ORDER BY draft desc, priority asc"; return $this->db->select($query, array_replace($roleValues, $params)); } - /** - * Add restrictions for a page query - * @param $query - * @param string $action - * @return mixed - */ - public function enforcePageRestrictions($query, $action = 'view') - { - // TODO - remove this - return $this->enforceEntityRestrictions('page', $query, $action); - } - - /** - * Add on permission restrictions to a chapter query. - * @param $query - * @param string $action - * @return mixed - */ - public function enforceChapterRestrictions($query, $action = 'view') - { - // TODO - remove this - return $this->enforceEntityRestrictions('chapter', $query, $action); - } - - /** - * Add restrictions to a book query. - * @param $query - * @param string $action - * @return mixed - */ - public function enforceBookRestrictions($query, $action = 'view') - { - // TODO - remove this - return $this->enforceEntityRestrictions('book', $query, $action); - } - /** * Add restrictions for a generic entity * @param string $entityType diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php index 79e574cbd..9fd4eb9ad 100644 --- a/tests/Entity/EntityTest.php +++ b/tests/Entity/EntityTest.php @@ -168,7 +168,7 @@ class EntityTest extends TestCase $entities = $this->createEntityChainBelongingToUser($creator, $updater); $this->actingAs($creator); app('BookStack\Repos\UserRepo')->destroy($creator); - app('BookStack\Repos\PageRepo')->saveRevision($entities['page']); + app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']); $this->checkEntitiesViewable($entities); } @@ -181,7 +181,7 @@ class EntityTest extends TestCase $entities = $this->createEntityChainBelongingToUser($creator, $updater); $this->actingAs($updater); app('BookStack\Repos\UserRepo')->destroy($updater); - app('BookStack\Repos\PageRepo')->saveRevision($entities['page']); + app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']); $this->checkEntitiesViewable($entities); } diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 1a46e30bc..233f300ee 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -4,13 +4,13 @@ class PageDraftTest extends TestCase { protected $page; - protected $pageRepo; + protected $entityRepo; public function setUp() { parent::setUp(); $this->page = \BookStack\Page::first(); - $this->pageRepo = app('\BookStack\Repos\PageRepo'); + $this->entityRepo = app('\BookStack\Repos\EntityRepo'); } public function test_draft_content_shows_if_available() @@ -20,7 +20,7 @@ class PageDraftTest extends TestCase ->dontSeeInField('html', $addedContent); $newContent = $this->page->html . $addedContent; - $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); + $this->entityRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->asAdmin()->visit($this->page->getUrl() . '/edit') ->seeInField('html', $newContent); } @@ -33,7 +33,7 @@ class PageDraftTest extends TestCase $newContent = $this->page->html . $addedContent; $newUser = $this->getEditor(); - $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); + $this->entityRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit') ->dontSeeInField('html', $newContent); } @@ -41,7 +41,7 @@ class PageDraftTest extends TestCase public function test_alert_message_shows_if_editing_draft() { $this->asAdmin(); - $this->pageRepo->saveUpdateDraft($this->page, ['html' => 'test content']); + $this->entityRepo->updatePageDraft($this->page, ['html' => 'test content']); $this->asAdmin()->visit($this->page->getUrl() . '/edit') ->see('You are currently editing a draft'); } @@ -55,7 +55,7 @@ class PageDraftTest extends TestCase $newContent = $this->page->html . $addedContent; $newUser = $this->getEditor(); - $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]); + $this->entityRepo->updatePageDraft($this->page, ['html' => $newContent]); $this->actingAs($newUser) ->visit($this->page->getUrl() . '/edit') diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index d78b94479..4784297a2 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -13,8 +13,8 @@ class SortTest extends TestCase public function test_drafts_do_not_show_up() { $this->asAdmin(); - $pageRepo = app('\BookStack\Repos\PageRepo'); - $draft = $pageRepo->getDraftPage($this->book); + $entityRepo = app('\BookStack\Repos\EntityRepo'); + $draft = $entityRepo->getDraftPage($this->book); $this->visit($this->book->getUrl()) ->see($draft->name) diff --git a/tests/ImageTest.php b/tests/ImageTest.php index 031517cdb..9da12d36b 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -90,7 +90,7 @@ class ImageTest extends TestCase 'type' => 'gallery' ]); - $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has been deleted'); + $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected'); } } \ No newline at end of file