/**
* 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);
}
/**
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use BookStack\Repos\EntityRepo;
-use BookStack\Repos\PageRepo;
use BookStack\Services\AttachmentService;
use Illuminate\Http\Request;
{
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();
}
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;
{
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();
}
'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());
}
{
$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]);
'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());
}
{
$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]);
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]);
}
// 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();
}
// Update permissions on changed models
- $this->bookRepo->buildJointPermissions($updatedModels);
+ $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}
$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');
}
{
$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());
}
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();
}
$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());
}
{
$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,
$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());
}
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]));
{
$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());
}
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
+use BookStack\Repos\EntityRepo;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
/**
* Deletes an image and all thumbnail/image files
- * @param PageRepo $pageRepo
+ * @param EntityRepo $entityRepo
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
- public function destroy(PageRepo $pageRepo, Request $request, $id)
+ public function destroy(EntityRepo $entityRepo, Request $request, $id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// 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);
}
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;
{
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();
// 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());
}
$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' => ''
]);
$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());
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());
// 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));
]);
$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());
}
], 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;
$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());
}
$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());
}
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()]));
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;
{
$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());
}
*/
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
*/
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
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]));
{
$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());
}
+++ /dev/null
-<?php namespace BookStack\Repos;
-
-use BookStack\Book;
-
-class BookRepo extends EntityRepo
-{
- protected $pageRepo;
- protected $chapterRepo;
-
- /**
- * BookRepo constructor.
- * @param PageRepo $pageRepo
- * @param ChapterRepo $chapterRepo
- */
- public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
- {
- $this->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
+++ /dev/null
-<?php namespace BookStack\Repos;
-
-
-use Activity;
-use BookStack\Book;
-use BookStack\Exceptions\NotFoundException;
-use Illuminate\Support\Str;
-use BookStack\Chapter;
-
-class ChapterRepo extends EntityRepo
-{
- protected $pageRepo;
-
- /**
- * ChapterRepo constructor.
- * @param $pageRepo
- */
- public function __construct(PageRepo $pageRepo)
- {
- $this->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
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
*/
public $page;
+ /**
+ * @var PageRevision
+ */
+ protected $pageRevision;
+
/**
* Base entity instances keyed by type
* @var []Entity
*/
protected $viewService;
+ /**
+ * @var TagRepo
+ */
+ protected $tagRepo;
+
/**
* Acceptable operators to be used in a query
* @var array
/**
* 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;
}
/**
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
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
->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), '/')));
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
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
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();
+ }
+
}
+++ /dev/null
-<?php namespace BookStack\Repos;
-
-use Activity;
-use BookStack\Book;
-use BookStack\Chapter;
-use BookStack\Entity;
-use BookStack\Exceptions\NotFoundException;
-use BookStack\Services\AttachmentService;
-use Carbon\Carbon;
-use DOMDocument;
-use DOMXPath;
-use Illuminate\Support\Str;
-use BookStack\Page;
-use BookStack\PageRevision;
-
-class PageRepo extends EntityRepo
-{
-
- protected $pageRevision;
- protected $tagRepo;
-
- /**
- * PageRepo constructor.
- * @param PageRevision $pageRevision
- * @param TagRepo $tagRepo
- */
- public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
- {
- $this->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);
- }
-
-}
{
$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();
}
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
$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);
}
$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);
}
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()
->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);
}
$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);
}
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');
}
$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')
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)
'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