diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 4f9f4c480..e7e75cc9f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,11 +1,13 @@ ### For Feature Requests + Desired Feature: ### For Bug Reports -PHP Version: -MySQL Version: +* BookStack Version: +* PHP Version: +* MySQL Version: -Expected Behavior: +##### Expected Behavior -Actual Behavior: +##### Actual Behavior diff --git a/.gitignore b/.gitignore index 919b3e75d..15b034ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ _ide_helper.php /storage/debugbar .phpstorm.meta.php yarn.lock +/bin \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index e2eb5f511..0ad753ced 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,7 @@ addons: before_script: - mysql -u root -e 'create database `bookstack-test`;' - - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN - phpenv config-rm xdebug.ini - - composer self-update - composer dump-autoload --no-interaction - composer install --prefer-dist --no-interaction - php artisan clear-compiled -n diff --git a/app/Chapter.php b/app/Chapter.php index cc5518b7a..dc23f5ebd 100644 --- a/app/Chapter.php +++ b/app/Chapter.php @@ -5,6 +5,8 @@ class Chapter extends Entity { protected $fillable = ['name', 'description', 'priority', 'book_id']; + protected $with = ['book']; + /** * Get the book this chapter is within. * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -16,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/Entity.php b/app/Entity.php index 186059f00..e8deddf0a 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -4,6 +4,8 @@ class Entity extends Ownable { + protected $fieldsToSearch = ['name', 'description']; + /** * Compares this entity to another given entity. * Matches by comparing class and id. @@ -157,7 +159,7 @@ class Entity extends Ownable * @param string[] array $wheres * @return mixed */ - public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) + public function fullTextSearchQuery($terms, $wheres = []) { $exactTerms = []; $fuzzyTerms = []; @@ -181,16 +183,16 @@ class Entity extends Ownable // Perform fulltext search if relevant terms exist. if ($isFuzzy) { $termString = implode(' ', $fuzzyTerms); - $fields = implode(',', $fieldsToSearch); + $fields = implode(',', $this->fieldsToSearch); $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); } // Ensure at least one exact term matches if in search if (count($exactTerms) > 0) { - $search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { + $search = $search->where(function ($query) use ($exactTerms) { foreach ($exactTerms as $exactTerm) { - foreach ($fieldsToSearch as $field) { + foreach ($this->fieldsToSearch as $field) { $query->orWhere($field, 'like', $exactTerm); } } diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 62be0b852..3c325d0fe 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -2,7 +2,7 @@ use BookStack\Exceptions\FileUploadException; use BookStack\Attachment; -use BookStack\Repos\PageRepo; +use BookStack\Repos\EntityRepo; use BookStack\Services\AttachmentService; use Illuminate\Http\Request; @@ -10,19 +10,19 @@ 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, PageRepo $pageRepo) + public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo) { $this->attachmentService = $attachmentService; $this->attachment = $attachment; - $this->pageRepo = $pageRepo; + $this->entityRepo = $entityRepo; parent::__construct(); } @@ -40,7 +40,7 @@ class AttachmentController extends Controller ]); $pageId = $request->get('uploaded_to'); - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $this->checkPermission('attachment-create-all'); $this->checkOwnablePermission('page-update', $page); @@ -70,14 +70,14 @@ class AttachmentController extends Controller ]); $pageId = $request->get('uploaded_to'); - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $attachment = $this->attachment->findOrFail($attachmentId); $this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('attachment-create', $attachment); if (intval($pageId) !== intval($attachment->uploaded_to)) { - return $this->jsonError('Page mismatch during attached file update'); + return $this->jsonError(trans('errors.attachment_page_mismatch')); } $uploadedFile = $request->file('file'); @@ -106,18 +106,18 @@ class AttachmentController extends Controller ]); $pageId = $request->get('uploaded_to'); - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $attachment = $this->attachment->findOrFail($attachmentId); $this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('attachment-create', $attachment); if (intval($pageId) !== intval($attachment->uploaded_to)) { - return $this->jsonError('Page mismatch during attachment update'); + return $this->jsonError(trans('errors.attachment_page_mismatch')); } $attachment = $this->attachmentService->updateFile($attachment, $request->all()); - return $attachment; + return response()->json($attachment); } /** @@ -134,7 +134,7 @@ class AttachmentController extends Controller ]); $pageId = $request->get('uploaded_to'); - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $this->checkPermission('attachment-create-all'); $this->checkOwnablePermission('page-update', $page); @@ -153,7 +153,7 @@ class AttachmentController extends Controller */ public function listForPage($pageId) { - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $this->checkOwnablePermission('page-view', $page); return response()->json($page->attachments); } @@ -170,12 +170,12 @@ class AttachmentController extends Controller 'files' => 'required|array', 'files.*.id' => 'required|integer', ]); - $page = $this->pageRepo->getById($pageId); + $page = $this->entityRepo->getById('page', $pageId); $this->checkOwnablePermission('page-update', $page); $attachments = $request->get('files'); $this->attachmentService->updateFileOrderWithinPage($attachments, $pageId); - return response()->json(['message' => 'Attachment order updated']); + return response()->json(['message' => trans('entities.attachments_order_updated')]); } /** @@ -186,7 +186,7 @@ class AttachmentController extends Controller public function get($attachmentId) { $attachment = $this->attachment->findOrFail($attachmentId); - $page = $this->pageRepo->getById($attachment->uploaded_to); + $page = $this->entityRepo->getById('page', $attachment->uploaded_to); $this->checkOwnablePermission('page-view', $page); if ($attachment->external) { @@ -210,6 +210,6 @@ class AttachmentController extends Controller $attachment = $this->attachment->findOrFail($attachmentId); $this->checkOwnablePermission('attachment-delete', $attachment); $this->attachmentService->deleteFile($attachment); - return response()->json(['message' => 'Attachment deleted']); + return response()->json(['message' => trans('entities.attachments_deleted')]); } } diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 45e40e6fe..d1fbddc50 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -52,7 +52,7 @@ class ForgotPasswordController extends Controller ); if ($response === Password::RESET_LINK_SENT) { - $message = 'A password reset link has been sent to ' . $request->get('email') . '.'; + $message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]); session()->flash('success', $message); return back()->with('status', trans($response)); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index c9d6a5496..e7eeb9bc1 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -87,7 +87,7 @@ class LoginController extends Controller // Check for users with same email already $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0; if ($alreadyUser) { - throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.'); + throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email])); } $user->save(); diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index d9bb500b4..8b0ef309a 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Controllers\Auth; use BookStack\Exceptions\ConfirmationEmailException; +use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\UserRegistrationException; use BookStack\Repos\UserRepo; use BookStack\Services\EmailConfirmationService; @@ -82,7 +83,7 @@ class RegisterController extends Controller protected function checkRegistrationAllowed() { if (!setting('registration-enabled')) { - throw new UserRegistrationException('Registrations are currently disabled.', '/login'); + throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login'); } } @@ -147,7 +148,7 @@ class RegisterController extends Controller $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict'))); $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1); if (!in_array($userEmailDomain, $restrictedEmailDomains)) { - throw new UserRegistrationException('That email domain does not have access to this application', '/register'); + throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register'); } } @@ -169,7 +170,7 @@ class RegisterController extends Controller } auth()->login($newUser); - session()->flash('success', 'Thanks for signing up! You are now registered and signed in.'); + session()->flash('success', trans('auth.register_success')); return redirect($this->redirectPath()); } @@ -262,7 +263,7 @@ class RegisterController extends Controller return $this->socialRegisterCallback($socialDriver); } } else { - throw new SocialSignInException('No action defined', '/login'); + throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login'); } return redirect()->back(); } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index bd64793f9..eb678503d 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -41,7 +41,7 @@ class ResetPasswordController extends Controller */ protected function sendResetResponse($response) { - $message = 'Your password has been successfully reset.'; + $message = trans('auth.reset_password_success'); session()->flash('success', $message); return redirect($this->redirectPath()) ->with('status', trans($response)); diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 8ada59433..57ac486d5 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -1,34 +1,26 @@ bookRepo = $bookRepo; - $this->pageRepo = $pageRepo; - $this->chapterRepo = $chapterRepo; + $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; parent::__construct(); } @@ -39,9 +31,9 @@ class BookController extends Controller */ public function index() { - $books = $this->bookRepo->getAllPaginated(10); - $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false; - $popular = $this->bookRepo->getPopular(4, 0); + $books = $this->entityRepo->getAllPaginated('book', 10); + $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false; + $popular = $this->entityRepo->getPopular('book', 4, 0); $this->setPageTitle('Books'); return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]); } @@ -53,7 +45,7 @@ class BookController extends Controller public function create() { $this->checkPermission('book-create-all'); - $this->setPageTitle('Create New Book'); + $this->setPageTitle(trans('entities.books_create')); return view('books/create'); } @@ -70,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()); } @@ -82,9 +74,9 @@ class BookController extends Controller */ public function show($slug) { - $book = $this->bookRepo->getBySlug($slug); + $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]); @@ -97,9 +89,9 @@ class BookController extends Controller */ public function edit($slug) { - $book = $this->bookRepo->getBySlug($slug); + $book = $this->entityRepo->getBySlug('book', $slug); $this->checkOwnablePermission('book-update', $book); - $this->setPageTitle('Edit Book ' . $book->getShortName()); + $this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()])); return view('books/edit', ['book' => $book, 'current' => $book]); } @@ -111,13 +103,13 @@ class BookController extends Controller */ public function update(Request $request, $slug) { - $book = $this->bookRepo->getBySlug($slug); + $book = $this->entityRepo->getBySlug('book', $slug); $this->checkOwnablePermission('book-update', $book); $this->validate($request, [ '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()); } @@ -129,9 +121,9 @@ class BookController extends Controller */ public function showDelete($bookSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-delete', $book); - $this->setPageTitle('Delete Book ' . $book->getShortName()); + $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()])); return view('books/delete', ['book' => $book, 'current' => $book]); } @@ -142,11 +134,11 @@ class BookController extends Controller */ public function sort($bookSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-update', $book); - $bookChildren = $this->bookRepo->getChildren($book, true); - $books = $this->bookRepo->getAll(false); - $this->setPageTitle('Sort Book ' . $book->getShortName()); + $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]); } @@ -158,8 +150,8 @@ class BookController extends Controller */ public function getSortItem($bookSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $bookChildren = $this->bookRepo->getChildren($book); + $book = $this->entityRepo->getBySlug('book', $bookSlug); + $bookChildren = $this->entityRepo->getBookChildren($book); return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); } @@ -171,7 +163,7 @@ class BookController extends Controller */ public function saveSort($bookSlug, Request $request) { - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-update', $book); // Return if no map sent @@ -190,13 +182,13 @@ class BookController extends Controller $priority = $bookChild->sort; $id = intval($bookChild->id); $isPage = $bookChild->type == 'page'; - $bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId; + $bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId; $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter); - $model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id); + $model = $this->entityRepo->getById($isPage?'page':'chapter', $id); // 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(); @@ -211,12 +203,12 @@ class BookController extends Controller // Add activity for books foreach ($sortedBooks as $bookId) { - $updatedBook = $this->bookRepo->getById($bookId); + $updatedBook = $this->entityRepo->getById('book', $bookId); Activity::add($updatedBook, 'book_sort', $updatedBook->id); } // Update permissions on changed models - $this->bookRepo->buildJointPermissions($updatedModels); + $this->entityRepo->buildJointPermissions($updatedModels); return redirect($book->getUrl()); } @@ -228,11 +220,10 @@ class BookController extends Controller */ public function destroy($bookSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); + $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'); } @@ -243,7 +234,7 @@ class BookController extends Controller */ public function showRestrict($bookSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('restrictions-manage', $book); $roles = $this->userRepo->getRestrictableRoles(); return view('books/restrictions', [ @@ -261,10 +252,10 @@ class BookController extends Controller */ public function restrict($bookSlug, Request $request) { - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('restrictions-manage', $book); - $this->bookRepo->updateEntityPermissionsFromRequest($request, $book); - session()->flash('success', 'Book Restrictions Updated'); + $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 a3fb600fd..1760ee5c6 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -1,30 +1,26 @@ bookRepo = $bookRepo; - $this->chapterRepo = $chapterRepo; + $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; parent::__construct(); } @@ -36,9 +32,9 @@ class ChapterController extends Controller */ public function create($bookSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('chapter-create', $book); - $this->setPageTitle('Create New Chapter'); + $this->setPageTitle(trans('entities.chapters_create')); return view('chapters/create', ['book' => $book, 'current' => $book]); } @@ -54,12 +50,12 @@ class ChapterController extends Controller 'name' => 'required|string|max:255' ]); - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); $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()); } @@ -72,15 +68,14 @@ class ChapterController extends Controller */ public function show($bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-view', $chapter); - $sidebarTree = $this->bookRepo->getChildren($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' => $book, + 'book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree, @@ -96,11 +91,10 @@ class ChapterController extends Controller */ public function edit($bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-update', $chapter); - $this->setPageTitle('Edit Chapter' . $chapter->getShortName()); - return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); + $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()])); + return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]); } /** @@ -112,16 +106,15 @@ class ChapterController extends Controller */ public function update(Request $request, $bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-update', $chapter); if ($chapter->name !== $request->get('name')) { - $chapter->slug = $this->chapterRepo->findSuitableSlug($request->get('name'), $book->id, $chapter->id); + $chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id); } $chapter->fill($request->all()); $chapter->updated_by = user()->id; $chapter->save(); - Activity::add($chapter, 'chapter_update', $book->id); + Activity::add($chapter, 'chapter_update', $chapter->book->id); return redirect($chapter->getUrl()); } @@ -133,11 +126,10 @@ class ChapterController extends Controller */ public function showDelete($bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-delete', $chapter); - $this->setPageTitle('Delete Chapter' . $chapter->getShortName()); - return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); + $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()])); + return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]); } /** @@ -148,11 +140,11 @@ class ChapterController extends Controller */ public function destroy($bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); + $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()); } @@ -164,12 +156,12 @@ class ChapterController extends Controller * @throws \BookStack\Exceptions\NotFoundException */ public function showMove($bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); + $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()])); $this->checkOwnablePermission('chapter-update', $chapter); return view('chapters/move', [ 'chapter' => $chapter, - 'book' => $book + 'book' => $chapter->book ]); } @@ -182,8 +174,7 @@ class ChapterController extends Controller * @throws \BookStack\Exceptions\NotFoundException */ public function move($bookSlug, $chapterSlug, Request $request) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-update', $chapter); $entitySelection = $request->get('entity_selection', null); @@ -198,17 +189,17 @@ class ChapterController extends Controller $parent = false; if ($entityType == 'book') { - $parent = $this->bookRepo->getById($entityId); + $parent = $this->entityRepo->getById('book', $entityId); } if ($parent === false || $parent === null) { - session()->flash('The selected Book was not found'); + session()->flash('error', trans('errors.selected_book_not_found')); 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', sprintf('Chapter moved to "%s"', $parent->name)); + session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name])); return redirect($chapter->getUrl()); } @@ -221,8 +212,7 @@ class ChapterController extends Controller */ public function showRestrict($bookSlug, $chapterSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $chapter); $roles = $this->userRepo->getRestrictableRoles(); return view('chapters/restrictions', [ @@ -240,11 +230,10 @@ class ChapterController extends Controller */ public function restrict($bookSlug, $chapterSlug, Request $request) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); + $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $chapter); - $this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter); - session()->flash('success', 'Chapter Restrictions Updated'); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter); + session()->flash('success', trans('entities.chapters_permissions_success')); return redirect($chapter->getUrl()); } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 2fc64b236..f4706a5c4 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers; use Activity; use BookStack\Repos\EntityRepo; use BookStack\Http\Requests; +use Illuminate\Http\Response; use Views; class HomeController extends Controller @@ -31,9 +32,9 @@ class HomeController extends Controller $activity = Activity::latest(10); $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : []; $recentFactor = count($draftPages) > 0 ? 0.5 : 1; - $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor); - $recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5); - $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5); + $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor); + $recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5); + $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5); return view('home', [ 'activity' => $activity, 'recents' => $recents, @@ -43,4 +44,39 @@ class HomeController extends Controller ]); } + /** + * Get a js representation of the current translations + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response + */ + public function getTranslations() { + $locale = trans()->getLocale(); + $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale; + if (cache()->has($cacheKey) && config('app.env') !== 'development') { + $resp = cache($cacheKey); + } else { + $translations = [ + // Get only translations which might be used in JS + 'common' => trans('common'), + 'components' => trans('components'), + 'entities' => trans('entities'), + 'errors' => trans('errors') + ]; + if ($locale !== 'en') { + $enTrans = [ + 'common' => trans('common', [], null, 'en'), + 'components' => trans('components', [], null, 'en'), + 'entities' => trans('entities', [], null, 'en'), + 'errors' => trans('errors', [], null, 'en') + ]; + $translations = array_replace_recursive($enTrans, $translations); + } + $resp = 'window.translations = ' . json_encode($translations); + cache()->put($cacheKey, $resp, 120); + } + + return response($resp, 200, [ + 'Content-Type' => 'application/javascript' + ]); + } + } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index 621c23e85..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); @@ -162,14 +164,14 @@ 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); } } $this->imageRepo->destroyImage($image); - return response()->json('Image Deleted'); + return response()->json(trans('components.images_deleted')); } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index c2d8e257c..4ed10d61e 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -2,40 +2,31 @@ use Activity; use BookStack\Exceptions\NotFoundException; +use BookStack\Repos\EntityRepo; use BookStack\Repos\UserRepo; use BookStack\Services\ExportService; use Carbon\Carbon; use Illuminate\Http\Request; -use BookStack\Http\Requests; -use BookStack\Repos\BookRepo; -use BookStack\Repos\ChapterRepo; -use BookStack\Repos\PageRepo; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Illuminate\Http\Response; use Views; use GatherContent\Htmldiff\Htmldiff; class PageController extends Controller { - protected $pageRepo; - protected $bookRepo; - protected $chapterRepo; + protected $entityRepo; protected $exportService; protected $userRepo; /** * PageController constructor. - * @param PageRepo $pageRepo - * @param BookRepo $bookRepo - * @param ChapterRepo $chapterRepo + * @param EntityRepo $entityRepo * @param ExportService $exportService * @param UserRepo $userRepo */ - public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo) + public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo) { - $this->pageRepo = $pageRepo; - $this->bookRepo = $bookRepo; - $this->chapterRepo = $chapterRepo; + $this->entityRepo = $entityRepo; $this->exportService = $exportService; $this->userRepo = $userRepo; parent::__construct(); @@ -50,19 +41,19 @@ class PageController extends Controller */ public function create($bookSlug, $chapterSlug = null) { - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; + $book = $this->entityRepo->getBySlug('book', $bookSlug); + $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null; $parent = $chapter ? $chapter : $book; $this->checkOwnablePermission('page-create', $parent); // 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()); } // Otherwise show edit view - $this->setPageTitle('Create New Page'); + $this->setPageTitle(trans('entities.pages_new')); return view('pages/guest-create', ['parent' => $parent]); } @@ -80,13 +71,13 @@ class PageController extends Controller 'name' => 'required|string|max:255' ]); - $book = $this->bookRepo->getBySlug($bookSlug); - $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; + $book = $this->entityRepo->getBySlug('book', $bookSlug); + $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null; $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' => '' ]); @@ -101,15 +92,14 @@ class PageController extends Controller */ public function editDraft($bookSlug, $pageId) { - $book = $this->bookRepo->getBySlug($bookSlug); - $draft = $this->pageRepo->getById($pageId, true); - $this->checkOwnablePermission('page-create', $book); - $this->setPageTitle('Edit Page Draft'); + $draft = $this->entityRepo->getById('page', $pageId, true); + $this->checkOwnablePermission('page-create', $draft->book); + $this->setPageTitle(trans('entities.pages_edit_draft')); $draftsEnabled = $this->signedIn; return view('pages/edit', [ 'page' => $draft, - 'book' => $book, + 'book' => $draft->book, 'isDraft' => true, 'draftsEnabled' => $draftsEnabled ]); @@ -119,6 +109,7 @@ class PageController extends Controller * Store a new page by changing a draft into a page. * @param Request $request * @param string $bookSlug + * @param int $pageId * @return Response */ public function store(Request $request, $bookSlug, $pageId) @@ -128,21 +119,21 @@ class PageController extends Controller ]); $input = $request->all(); - $book = $this->bookRepo->getBySlug($bookSlug); + $book = $this->entityRepo->getBySlug('book', $bookSlug); - $draftPage = $this->pageRepo->getById($pageId, true); + $draftPage = $this->entityRepo->getById('page', $pageId, true); $chapterId = intval($draftPage->chapter_id); - $parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book; + $parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book; $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()); @@ -150,33 +141,33 @@ class PageController extends Controller /** * Display the specified page. - * If the page is not found via the slug the - * revisions are searched for a match. + * If the page is not found via the slug the revisions are searched for a match. * @param string $bookSlug * @param string $pageSlug * @return Response */ public function show($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - try { - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $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($book); - $pageNav = $this->pageRepo->getPageNav($page); + $pageContent = $this->entityRepo->renderPage($page); + $sidebarTree = $this->entityRepo->getBookChildren($page->book); + $pageNav = $this->entityRepo->getPageNav($pageContent); Views::add($page); $this->setPageTitle($page->getShortName()); - return view('pages/show', ['page' => $page, 'book' => $book, - 'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]); + return view('pages/show', [ + 'page' => $page,'book' => $page->book, + 'current' => $page, 'sidebarTree' => $sidebarTree, + 'pageNav' => $pageNav, 'pageContent' => $pageContent]); } /** @@ -186,7 +177,7 @@ class PageController extends Controller */ public function getPageAjax($pageId) { - $page = $this->pageRepo->getById($pageId); + $page = $this->entityRepo->getById('page', $pageId); return response()->json($page); } @@ -198,26 +189,25 @@ class PageController extends Controller */ public function edit($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $this->setPageTitle('Editing Page ' . $page->getShortName()); + $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()])); $page->isDraft = false; // 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)); @@ -225,7 +215,7 @@ class PageController extends Controller $draftsEnabled = $this->signedIn; return view('pages/edit', [ 'page' => $page, - 'book' => $book, + 'book' => $page->book, 'current' => $page, 'draftsEnabled' => $draftsEnabled ]); @@ -243,11 +233,10 @@ class PageController extends Controller $this->validate($request, [ 'name' => 'required|string|max:255' ]); - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $this->pageRepo->updatePage($page, $book->id, $request->all()); - Activity::add($page, 'page_update', $book->id); + $this->entityRepo->updatePage($page, $page->book->id, $request->all()); + Activity::add($page, 'page_update', $page->book->id); return redirect($page->getUrl()); } @@ -259,27 +248,23 @@ class PageController extends Controller */ public function saveDraft(Request $request, $pageId) { - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $this->checkOwnablePermission('page-update', $page); if (!$this->signedIn) { return response()->json([ 'status' => 'error', - 'message' => 'Guests cannot save drafts', + 'message' => trans('errors.guests_cannot_save_drafts'), ], 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; return response()->json([ 'status' => 'success', - 'message' => 'Draft saved at ', + 'message' => trans('entities.pages_edit_draft_save_at'), 'timestamp' => $utcUpdateTimestamp ]); } @@ -292,7 +277,7 @@ class PageController extends Controller */ public function redirectFromLink($pageId) { - $page = $this->pageRepo->getById($pageId); + $page = $this->entityRepo->getById('page', $pageId); return redirect($page->getUrl()); } @@ -304,11 +289,10 @@ class PageController extends Controller */ public function showDelete($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-delete', $page); - $this->setPageTitle('Delete Page ' . $page->getShortName()); - return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); + $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()])); + return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]); } @@ -321,11 +305,10 @@ class PageController extends Controller */ public function showDeleteDraft($bookSlug, $pageId) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); $this->checkOwnablePermission('page-update', $page); - $this->setPageTitle('Delete Draft Page ' . $page->getShortName()); - return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); + $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()])); + return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]); } /** @@ -337,12 +320,12 @@ class PageController extends Controller */ public function destroy($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $book = $page->book; $this->checkOwnablePermission('page-delete', $page); Activity::addMessage('page_delete', $book->id, $page->name); - session()->flash('success', 'Page deleted'); - $this->pageRepo->destroy($page); + session()->flash('success', trans('entities.pages_delete_success')); + $this->entityRepo->destroyPage($page); return redirect($book->getUrl()); } @@ -355,11 +338,11 @@ class PageController extends Controller */ public function destroyDraft($bookSlug, $pageId) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getById($pageId, true); + $page = $this->entityRepo->getById('page', $pageId, true); + $book = $page->book; $this->checkOwnablePermission('page-update', $page); - session()->flash('success', 'Draft deleted'); - $this->pageRepo->destroy($page); + session()->flash('success', trans('entities.pages_delete_draft_success')); + $this->entityRepo->destroyPage($page); return redirect($book->getUrl()); } @@ -371,10 +354,9 @@ class PageController extends Controller */ public function showRevisions($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); - $this->setPageTitle('Revisions For ' . $page->getShortName()); - return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()])); + return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); } /** @@ -386,16 +368,15 @@ class PageController extends Controller */ public function showRevision($bookSlug, $pageSlug, $revisionId) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); - $revision = $this->pageRepo->getRevisionById($revisionId); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $revision = $this->entityRepo->getById('page_revision', $revisionId, false); $page->fill($revision->toArray()); - $this->setPageTitle('Page Revision For ' . $page->getShortName()); + $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()])); return view('pages/revision', [ 'page' => $page, - 'book' => $book, + 'book' => $page->book, ]); } @@ -408,20 +389,19 @@ class PageController extends Controller */ public function showRevisionChanges($bookSlug, $pageSlug, $revisionId) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); - $revision = $this->pageRepo->getRevisionById($revisionId); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $revision = $this->entityRepo->getById('page_revision', $revisionId); $prev = $revision->getPrevious(); $prevContent = ($prev === null) ? '' : $prev->html; $diff = (new Htmldiff)->diff($prevContent, $revision->html); $page->fill($revision->toArray()); - $this->setPageTitle('Page Revision For ' . $page->getShortName()); + $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()])); return view('pages/revision', [ 'page' => $page, - 'book' => $book, + 'book' => $page->book, 'diff' => $diff, ]); } @@ -435,11 +415,10 @@ class PageController extends Controller */ public function restoreRevision($bookSlug, $pageSlug, $revisionId) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $page = $this->pageRepo->restoreRevision($page, $book, $revisionId); - Activity::add($page, 'page_restore', $book->id); + $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId); + Activity::add($page, 'page_restore', $page->book->id); return redirect($page->getUrl()); } @@ -452,9 +431,9 @@ class PageController extends Controller */ public function exportPdf($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $pdfContent = $this->exportService->pageToPdf($page); +// return $pdfContent; return response()->make($pdfContent, 200, [ 'Content-Type' => 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' @@ -469,8 +448,7 @@ class PageController extends Controller */ public function exportHtml($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $containedHtml = $this->exportService->pageToContainedHtml($page); return response()->make($containedHtml, 200, [ 'Content-Type' => 'application/octet-stream', @@ -486,8 +464,7 @@ class PageController extends Controller */ public function exportPlainText($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $containedHtml = $this->exportService->pageToPlainText($page); return response()->make($containedHtml, 200, [ 'Content-Type' => 'application/octet-stream', @@ -501,9 +478,9 @@ 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' => 'Recently Created Pages', + 'title' => trans('entities.recently_created_pages'), 'pages' => $pages ]); } @@ -514,9 +491,9 @@ 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' => 'Recently Updated Pages', + 'title' => trans('entities.recently_updated_pages'), 'pages' => $pages ]); } @@ -529,8 +506,7 @@ class PageController extends Controller */ public function showRestrict($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $page); $roles = $this->userRepo->getRestrictableRoles(); return view('pages/restrictions', [ @@ -548,11 +524,10 @@ class PageController extends Controller */ public function showMove($bookSlug, $pageSlug) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); return view('pages/move', [ - 'book' => $book, + 'book' => $page->book, 'page' => $page ]); } @@ -567,8 +542,7 @@ class PageController extends Controller */ public function move($bookSlug, $pageSlug, Request $request) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); $entitySelection = $request->get('entity_selection', null); @@ -580,22 +554,17 @@ class PageController extends Controller $entityType = $stringExploded[0]; $entityId = intval($stringExploded[1]); - $parent = false; - if ($entityType == 'chapter') { - $parent = $this->chapterRepo->getById($entityId); - } else if ($entityType == 'book') { - $parent = $this->bookRepo->getById($entityId); - } - - if ($parent === false || $parent === null) { - session()->flash('The selected Book or Chapter was not found'); + try { + $parent = $this->entityRepo->getById($entityType, $entityId); + } catch (\Exception $e) { + session()->flash(trans('entities.selected_book_chapter_not_found')); return redirect()->back(); } - $this->pageRepo->changePageParent($page, $parent); + $this->entityRepo->changePageParent($page, $parent); Activity::add($page, 'page_move', $page->book->id); - session()->flash('success', sprintf('Page moved to "%s"', $parent->name)); + session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name])); return redirect($page->getUrl()); } @@ -609,11 +578,10 @@ class PageController extends Controller */ public function restrict($bookSlug, $pageSlug, Request $request) { - $book = $this->bookRepo->getBySlug($bookSlug); - $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $page); - $this->pageRepo->updateEntityPermissionsFromRequest($request, $page); - session()->flash('success', 'Page Permissions Updated'); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $page); + session()->flash('success', trans('entities.pages_permissions_success')); return redirect($page->getUrl()); } diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php index ed430c0b7..cd064e7e8 100644 --- a/app/Http/Controllers/PermissionController.php +++ b/app/Http/Controllers/PermissionController.php @@ -2,9 +2,7 @@ use BookStack\Exceptions\PermissionsException; use BookStack\Repos\PermissionsRepo; -use BookStack\Services\PermissionService; use Illuminate\Http\Request; -use BookStack\Http\Requests; class PermissionController extends Controller { @@ -55,7 +53,7 @@ class PermissionController extends Controller ]); $this->permissionsRepo->saveNewRole($request->all()); - session()->flash('success', 'Role successfully created'); + session()->flash('success', trans('settings.role_create_success')); return redirect('/settings/roles'); } @@ -69,7 +67,7 @@ class PermissionController extends Controller { $this->checkPermission('user-roles-manage'); $role = $this->permissionsRepo->getRoleById($id); - if ($role->hidden) throw new PermissionsException('This role cannot be edited'); + if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited')); return view('settings/roles/edit', ['role' => $role]); } @@ -88,7 +86,7 @@ class PermissionController extends Controller ]); $this->permissionsRepo->updateRole($id, $request->all()); - session()->flash('success', 'Role successfully updated'); + session()->flash('success', trans('settings.role_update_success')); return redirect('/settings/roles'); } @@ -103,7 +101,7 @@ class PermissionController extends Controller $this->checkPermission('user-roles-manage'); $role = $this->permissionsRepo->getRoleById($id); $roles = $this->permissionsRepo->getAllRolesExcept($role); - $blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']); + $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); $roles->prepend($blankRole); return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]); } @@ -126,7 +124,7 @@ class PermissionController extends Controller return redirect()->back(); } - session()->flash('success', 'Role successfully deleted'); + session()->flash('success', trans('settings.role_delete_success')); return redirect('/settings/roles'); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 58ad737c4..37aaccece 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -1,34 +1,22 @@ -pageRepo = $pageRepo; - $this->bookRepo = $bookRepo; - $this->chapterRepo = $chapterRepo; + $this->entityRepo = $entityRepo; $this->viewService = $viewService; parent::__construct(); } @@ -46,10 +34,10 @@ class SearchController extends Controller } $searchTerm = $request->get('term'); $paginationAppends = $request->only('term'); - $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); - $books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends); - $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends); - $this->setPageTitle('Search For ' . $searchTerm); + $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends); + $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends); + $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends); + $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm])); return view('search/all', [ 'pages' => $pages, 'books' => $books, @@ -69,11 +57,11 @@ class SearchController extends Controller $searchTerm = $request->get('term'); $paginationAppends = $request->only('term'); - $pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); - $this->setPageTitle('Page Search For ' . $searchTerm); + $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends); + $this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm])); return view('search/entity-search-list', [ 'entities' => $pages, - 'title' => 'Page Search Results', + 'title' => trans('entities.search_results_page'), 'searchTerm' => $searchTerm ]); } @@ -89,11 +77,11 @@ class SearchController extends Controller $searchTerm = $request->get('term'); $paginationAppends = $request->only('term'); - $chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends); - $this->setPageTitle('Chapter Search For ' . $searchTerm); + $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends); + $this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm])); return view('search/entity-search-list', [ 'entities' => $chapters, - 'title' => 'Chapter Search Results', + 'title' => trans('entities.search_results_chapter'), 'searchTerm' => $searchTerm ]); } @@ -109,11 +97,11 @@ class SearchController extends Controller $searchTerm = $request->get('term'); $paginationAppends = $request->only('term'); - $books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends); - $this->setPageTitle('Book Search For ' . $searchTerm); + $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends); + $this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm])); return view('search/entity-search-list', [ 'entities' => $books, - 'title' => 'Book Search Results', + 'title' => trans('entities.search_results_book'), 'searchTerm' => $searchTerm ]); } @@ -132,8 +120,8 @@ class SearchController extends Controller } $searchTerm = $request->get('term'); $searchWhereTerms = [['book_id', '=', $bookId]]; - $pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms); - $chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms); + $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms); + $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms); return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); } @@ -152,9 +140,11 @@ class SearchController extends Controller // Search for entities otherwise show most popular if ($searchTerm !== false) { - if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items()); - if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items()); - if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items()); + foreach (['page', 'chapter', 'book'] as $entityType) { + if ($entityTypes->contains($entityType)) { + $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items()); + } + } $entities = $entities->sortByDesc('title_relevance'); } else { $entityNames = $entityTypes->map(function ($type) { diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 65135eda3..70a12631a 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,8 +1,7 @@ flash('success', 'Settings Saved'); + session()->flash('success', trans('settings.settings_save_success')); return redirect('/settings'); } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index c8a356541..24bdcdb1c 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -2,7 +2,6 @@ use BookStack\Repos\TagRepo; use Illuminate\Http\Request; -use BookStack\Http\Requests; class TagController extends Controller { @@ -16,12 +15,14 @@ class TagController extends Controller public function __construct(TagRepo $tagRepo) { $this->tagRepo = $tagRepo; + parent::__construct(); } /** * Get all the Tags for a particular entity * @param $entityType * @param $entityId + * @return \Illuminate\Http\JsonResponse */ public function getForEntity($entityType, $entityId) { @@ -29,29 +30,10 @@ class TagController extends Controller return response()->json($tags); } - /** - * Update the tags for a particular entity. - * @param $entityType - * @param $entityId - * @param Request $request - * @return mixed - */ - public function updateForEntity($entityType, $entityId, Request $request) - { - $entity = $this->tagRepo->getEntity($entityType, $entityId, 'update'); - if ($entity === null) return $this->jsonError("Entity not found", 404); - - $inputTags = $request->input('tags'); - $tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags); - return response()->json([ - 'tags' => $tags, - 'message' => 'Tags successfully updated' - ]); - } - /** * Get tag name suggestions from a given search term. * @param Request $request + * @return \Illuminate\Http\JsonResponse */ public function getNameSuggestions(Request $request) { @@ -63,6 +45,7 @@ class TagController extends Controller /** * Get tag value suggestions from a given search term. * @param Request $request + * @return \Illuminate\Http\JsonResponse */ public function getValueSuggestions(Request $request) { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 18ef1a671..c98d5f87e 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -1,13 +1,8 @@ - $request->has('sort') ? $request->get('sort') : 'name', ]; $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails); - $this->setPageTitle('Users'); + $this->setPageTitle(trans('settings.users')); $users->appends($listDetails); return view('users/index', ['users' => $users, 'listDetails' => $listDetails]); } @@ -83,7 +78,6 @@ class UserController extends Controller } $this->validate($request, $validationRules); - $user = $this->user->fill($request->all()); if ($authMethod === 'standard') { @@ -131,7 +125,7 @@ class UserController extends Controller $authMethod = ($user->system_name) ? 'system' : config('auth.method'); $activeSocialDrivers = $socialAuthService->getActiveDrivers(); - $this->setPageTitle('User Profile'); + $this->setPageTitle(trans('settings.user_profile')); $roles = $this->userRepo->getAllRoles(); return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]); } @@ -153,9 +147,8 @@ class UserController extends Controller 'name' => 'min:2', 'email' => 'min:2|email|unique:users,email,' . $id, 'password' => 'min:5|required_with:password_confirm', - 'password-confirm' => 'same:password|required_with:password' - ], [ - 'password-confirm.required_with' => 'Password confirmation required' + 'password-confirm' => 'same:password|required_with:password', + 'setting' => 'array' ]); $user = $this->user->findOrFail($id); @@ -178,8 +171,15 @@ class UserController extends Controller $user->external_auth_id = $request->get('external_auth_id'); } + // Save an user-specific settings + if ($request->has('setting')) { + foreach ($request->get('setting') as $key => $value) { + setting()->putUser($user, $key, $value); + } + } + $user->save(); - session()->flash('success', 'User successfully updated'); + session()->flash('success', trans('settings.users_edit_success')); $redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id; return redirect($redirectUrl); @@ -197,7 +197,7 @@ class UserController extends Controller }); $user = $this->user->findOrFail($id); - $this->setPageTitle('Delete User ' . $user->name); + $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); return view('users/delete', ['user' => $user]); } @@ -216,17 +216,17 @@ class UserController extends Controller $user = $this->userRepo->getById($id); if ($this->userRepo->isOnlyAdmin($user)) { - session()->flash('error', 'You cannot delete the only admin'); + session()->flash('error', trans('errors.users_cannot_delete_only_admin')); return redirect($user->getEditUrl()); } if ($user->system_name === 'public') { - session()->flash('error', 'You cannot delete the guest user'); + session()->flash('error', trans('errors.users_cannot_delete_guest')); return redirect($user->getEditUrl()); } $this->userRepo->destroy($user); - session()->flash('success', 'User successfully removed'); + session()->flash('success', trans('settings.users_delete_success')); return redirect('/settings/users'); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index f1d95f5c0..c55cc9ab8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -1,6 +1,4 @@ - [ 'throttle:60,1', diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 8461ed0ba..b78016688 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -4,8 +4,6 @@ namespace BookStack\Http\Middleware; use Closure; use Illuminate\Contracts\Auth\Guard; -use BookStack\Exceptions\UserRegistrationException; -use Setting; class Authenticate { diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php new file mode 100644 index 000000000..31cb5d9a2 --- /dev/null +++ b/app/Http/Middleware/Localization.php @@ -0,0 +1,23 @@ +getUser(user(), 'language', $defaultLang); + app()->setLocale($locale); + Carbon::setLocale($locale); + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 2b3c64695..c27df7af4 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -1,6 +1,4 @@ -line('You are receiving this email because we received a password reset request for your account.') - ->action('Reset Password', baseUrl('password/reset/' . $this->token)) - ->line('If you did not request a password reset, no further action is required.'); + ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')])) + ->line(trans('auth.email_reset_text')) + ->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token)) + ->line(trans('auth.email_reset_not_requested')); } } diff --git a/app/Page.php b/app/Page.php index 3ee9e90f4..b24e7778a 100644 --- a/app/Page.php +++ b/app/Page.php @@ -7,6 +7,10 @@ class Page extends Entity protected $simpleAttributes = ['name', 'id', 'slug']; + protected $with = ['book']; + + protected $fieldsToSearch = ['name', 'text']; + /** * Converts this page into a simplified array. * @return mixed diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4665bf6c7..40a1eef3d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -1,6 +1,7 @@ getMimeType(), $imageMimes); }); - } /** diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php deleted file mode 100644 index 7bb91f472..000000000 --- a/app/Repos/BookRepo.php +++ /dev/null @@ -1,295 +0,0 @@ -pageRepo = $pageRepo; - $this->chapterRepo = $chapterRepo; - parent::__construct(); - } - - /** - * Base query for getting books. - * Takes into account any restrictions. - * @return mixed - */ - private function bookQuery() - { - return $this->permissionService->enforceBookRestrictions($this->book, 'view'); - } - - /** - * Get the book that has the given id. - * @param $id - * @return mixed - */ - public function getById($id) - { - return $this->bookQuery()->findOrFail($id); - } - - /** - * Get all books, Limited by count. - * @param int $count - * @return mixed - */ - public function getAll($count = 10) - { - $bookQuery = $this->bookQuery()->orderBy('name', 'asc'); - if (!$count) return $bookQuery->get(); - return $bookQuery->take($count)->get(); - } - - /** - * Get all books paginated. - * @param int $count - * @return mixed - */ - public function getAllPaginated($count = 10) - { - return $this->bookQuery() - ->orderBy('name', 'asc')->paginate($count); - } - - - /** - * Get the latest books. - * @param int $count - * @return mixed - */ - public function getLatest($count = 10) - { - return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get(); - } - - /** - * Gets the most recently viewed for a user. - * @param int $count - * @param int $page - * @return mixed - */ - public function getRecentlyViewed($count = 10, $page = 0) - { - return Views::getUserRecentlyViewed($count, $page, $this->book); - } - - /** - * Gets the most viewed books. - * @param int $count - * @param int $page - * @return mixed - */ - public function getPopular($count = 10, $page = 0) - { - return Views::getPopular($count, $page, $this->book); - } - - /** - * Get a book by slug - * @param $slug - * @return mixed - * @throws NotFoundException - */ - public function getBySlug($slug) - { - $book = $this->bookQuery()->where('slug', '=', $slug)->first(); - if ($book === null) throw new NotFoundException('Book not found'); - return $book; - } - - /** - * Checks if a book exists. - * @param $id - * @return bool - */ - public function exists($id) - { - return $this->bookQuery()->where('id', '=', $id)->exists(); - } - - /** - * 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->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($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; - } - - /** - * @param string $slug - * @param bool|false $currentId - * @return bool - */ - public function doesSlugExist($slug, $currentId = false) - { - $query = $this->book->where('slug', '=', $slug); - if ($currentId) { - $query = $query->where('id', '!=', $currentId); - } - return $query->count() > 0; - } - - /** - * Provides a suitable slug for the given book name. - * Ensures the returned slug is unique in the system. - * @param string $name - * @param bool|false $currentId - * @return string - */ - public function findSuitableSlug($name, $currentId = false) - { - $slug = $this->nameToSlug($name); - while ($this->doesSlugExist($slug, $currentId)) { - $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); - } - return $slug; - } - - /** - * 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) - { - $pageQuery = $book->pages()->where('chapter_id', '=', 0); - $pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view'); - - if ($filterDrafts) { - $pageQuery = $pageQuery->where('draft', '=', false); - } - - $pages = $pageQuery->get(); - - $chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) { - $this->permissionService->enforcePageRestrictions($query, 'view'); - if ($filterDrafts) $query->where('draft', '=', false); - }]); - $chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view'); - $chapters = $chapterQuery->get(); - $children = $pages->values(); - foreach ($chapters as $chapter) { - $children->push($chapter); - } - $bookSlug = $book->slug; - - $children->each(function ($child) use ($bookSlug) { - $child->setAttribute('bookSlug', $bookSlug); - if ($child->isA('chapter')) { - $child->pages->each(function ($page) use ($bookSlug) { - $page->setAttribute('bookSlug', $bookSlug); - }); - $child->pages = $child->pages->sortBy(function ($child, $key) { - $score = $child->priority; - if ($child->draft) $score -= 100; - return $score; - }); - } - }); - - // Sort items with drafts first then by priority. - return $children->sortBy(function ($child, $key) { - $score = $child->priority; - if ($child->isA('page') && $child->draft) $score -= 100; - return $score; - }); - } - - /** - * Get books by search term. - * @param $term - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($term, $count = 20, $paginationAppends = []) - { - $terms = $this->prepareSearchTerms($term); - $bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms)); - $bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term); - $books = $bookQuery->paginate($count)->appends($paginationAppends); - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - foreach ($books as $book) { - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $book->getExcerpt(100)); - $book->searchSnippet = $result; - } - return $books; - } - -} \ No newline at end of file diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php deleted file mode 100644 index 4c13b9aaf..000000000 --- a/app/Repos/ChapterRepo.php +++ /dev/null @@ -1,226 +0,0 @@ -pageRepo = $pageRepo; - parent::__construct(); - } - - /** - * Base query for getting chapters, Takes permissions into account. - * @return mixed - */ - private function chapterQuery() - { - return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view'); - } - - /** - * Check if an id exists. - * @param $id - * @return bool - */ - public function idExists($id) - { - return $this->chapterQuery()->where('id', '=', $id)->count() > 0; - } - - /** - * Get a chapter by a specific id. - * @param $id - * @return mixed - */ - public function getById($id) - { - return $this->chapterQuery()->findOrFail($id); - } - - /** - * Get all chapters. - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public function getAll() - { - return $this->chapterQuery()->all(); - } - - /** - * Get a chapter that has the given slug within the given book. - * @param $slug - * @param $bookId - * @return mixed - * @throws NotFoundException - */ - public function getBySlug($slug, $bookId) - { - $chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); - if ($chapter === null) throw new NotFoundException('Chapter not found'); - return $chapter; - } - - /** - * 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->name, $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(); - } - - /** - * Check if a chapter's slug exists. - * @param $slug - * @param $bookId - * @param bool|false $currentId - * @return bool - */ - public function doesSlugExist($slug, $bookId, $currentId = false) - { - $query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId); - if ($currentId) { - $query = $query->where('id', '!=', $currentId); - } - return $query->count() > 0; - } - - /** - * Finds a suitable slug for the provided name. - * Checks database to prevent duplicate slugs. - * @param $name - * @param $bookId - * @param bool|false $currentId - * @return string - */ - public function findSuitableSlug($name, $bookId, $currentId = false) - { - $slug = $this->nameToSlug($name); - while ($this->doesSlugExist($slug, $bookId, $currentId)) { - $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); - } - return $slug; - } - - /** - * 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; - } - - /** - * Get chapters by the given search term. - * @param string $term - * @param array $whereTerms - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) - { - $terms = $this->prepareSearchTerms($term); - $chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms)); - $chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term); - $chapters = $chapterQuery->paginate($count)->appends($paginationAppends); - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - foreach ($chapters as $chapter) { - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $chapter->getExcerpt(100)); - $chapter->searchSnippet = $result; - } - return $chapters; - } - - /** - * 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->name, $bookId, $chapter->id); - $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 7ecfb758c..f1428735c 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -3,11 +3,16 @@ use BookStack\Book; 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\User; +use BookStack\Services\ViewService; +use Carbon\Carbon; +use DOMDocument; +use DOMXPath; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Log; class EntityRepo { @@ -27,11 +32,32 @@ class EntityRepo */ public $page; + /** + * @var PageRevision + */ + protected $pageRevision; + + /** + * Base entity instances keyed by type + * @var []Entity + */ + protected $entities; + /** * @var PermissionService */ protected $permissionService; + /** + * @var ViewService + */ + protected $viewService; + + /** + * @var TagRepo + */ + protected $tagRepo; + /** * Acceptable operators to be used in a query * @var array @@ -40,26 +66,163 @@ 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 + ) { - $this->book = app(Book::class); - $this->chapter = app(Chapter::class); - $this->page = app(Page::class); - $this->permissionService = app(PermissionService::class); + $this->book = $book; + $this->chapter = $chapter; + $this->page = $page; + $this->pageRevision = $pageRevision; + $this->entities = [ + 'page' => $this->page, + 'chapter' => $this->chapter, + 'book' => $this->book, + 'page_revision' => $this->pageRevision + ]; + $this->viewService = $viewService; + $this->permissionService = $permissionService; + $this->tagRepo = $tagRepo; } /** - * Get the latest books added to the system. + * Get an entity instance via type. + * @param $type + * @return Entity + */ + protected function getEntity($type) + { + return $this->entities[strtolower($type)]; + } + + /** + * Base query for searching entities via permission system + * @param string $type + * @param bool $allowDrafts + * @return \Illuminate\Database\Query\Builder + */ + protected function entityQuery($type, $allowDrafts = false) + { + $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view'); + if (strtolower($type) === 'page' && !$allowDrafts) { + $q = $q->where('draft', '=', false); + } + return $q; + } + + /** + * Check if an entity with the given id exists. + * @param $type + * @param $id + * @return bool + */ + public function exists($type, $id) + { + return $this->entityQuery($type)->where('id', '=', $id)->exists(); + } + + /** + * Get an entity by ID + * @param string $type + * @param integer $id + * @param bool $allowDrafts + * @return Entity + */ + public function getById($type, $id, $allowDrafts = false) + { + return $this->entityQuery($type, $allowDrafts)->find($id); + } + + /** + * Get an entity by its url slug. + * @param string $type + * @param string $slug + * @param string|bool $bookSlug + * @return Entity + * @throws NotFoundException + */ + public function getBySlug($type, $slug, $bookSlug = false) + { + $q = $this->entityQuery($type)->where('slug', '=', $slug); + + if (strtolower($type) === 'chapter' || strtolower($type) === 'page') { + $q = $q->where('book_id', '=', function($query) use ($bookSlug) { + $query->select('id') + ->from($this->book->getTable()) + ->where('slug', '=', $bookSlug)->limit(1); + }); + } + $entity = $q->first(); + if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found')); + 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 + * @param integer|bool $count + * @return Collection + */ + public function getAll($type, $count = 20) + { + $q = $this->entityQuery($type)->orderBy('name', 'asc'); + if ($count !== false) $q = $q->take($count); + return $q->get(); + } + + /** + * Get all entities in a paginated format + * @param $type + * @param int $count + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getAllPaginated($type, $count = 10) + { + return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count); + } + + /** + * Get the most recently created entities of the given type. + * @param string $type * @param int $count * @param int $page - * @param bool $additionalQuery - * @return + * @param bool|callable $additionalQuery */ - public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false) + public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false) { - $query = $this->permissionService->enforceBookRestrictions($this->book) + $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)) ->orderBy('created_at', 'desc'); + if (strtolower($type) === 'page') $query = $query->where('draft', '=', false); if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); } @@ -67,45 +230,17 @@ class EntityRepo } /** - * Get the most recently updated books. - * @param $count - * @param int $page - * @return mixed - */ - public function getRecentlyUpdatedBooks($count = 20, $page = 0) - { - return $this->permissionService->enforceBookRestrictions($this->book) - ->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get(); - } - - /** - * Get the latest pages added to the system. + * Get the most recently updated entities of the given type. + * @param string $type * @param int $count * @param int $page - * @param bool $additionalQuery - * @return + * @param bool|callable $additionalQuery */ - public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false) + public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false) { - $query = $this->permissionService->enforcePageRestrictions($this->page) - ->orderBy('created_at', 'desc')->where('draft', '=', false); - if ($additionalQuery !== false && is_callable($additionalQuery)) { - $additionalQuery($query); - } - return $query->with('book')->skip($page * $count)->take($count)->get(); - } - - /** - * Get the latest chapters added to the system. - * @param int $count - * @param int $page - * @param bool $additionalQuery - * @return - */ - public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false) - { - $query = $this->permissionService->enforceChapterRestrictions($this->chapter) - ->orderBy('created_at', 'desc'); + $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)) + ->orderBy('updated_at', 'desc'); + if (strtolower($type) === 'page') $query = $query->where('draft', '=', false); if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); } @@ -113,16 +248,51 @@ class EntityRepo } /** - * Get the most recently updated pages. - * @param $count + * Get the most recently viewed entities. + * @param string|bool $type + * @param int $count * @param int $page * @return mixed */ - public function getRecentlyUpdatedPages($count = 20, $page = 0) + public function getRecentlyViewed($type, $count = 10, $page = 0) { - return $this->permissionService->enforcePageRestrictions($this->page) - ->where('draft', '=', false) - ->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get(); + $filter = is_bool($type) ? false : $this->getEntity($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 + * @param int $count + * @param int $page + * @return mixed + */ + public function getPopular($type, $count = 10, $page = 0) + { + $filter = is_bool($type) ? false : $this->getEntity($type); + return $this->viewService->getPopular($count, $page, $filter); } /** @@ -138,6 +308,163 @@ 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)->get(); + $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->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), '/'))); + + // Highlight page content + if ($type === 'page') { + //lookahead/behind assertions ensures cut between words + $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words + + foreach ($entities as $page) { + preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER); + //delimiter between occurrences + $results = []; + foreach ($matches as $line) { + $results[] = htmlspecialchars($line[0], 0, 'UTF-8'); + } + $matchLimit = 6; + if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit); + $result = join('... ', $results); + + //highlight + $result = preg_replace('#' . $words . '#iu', "\$0", $result); + if (strlen($result) < 5) $result = $page->getExcerpt(80); + + $page->searchSnippet = $result; + } + return $entities; + } + + // Highlight chapter/book content + foreach ($entities as $entity) { + //highlight + $result = preg_replace('#' . $words . '#iu', "\$0", $entity->getExcerpt(100)); + $entity->searchSnippet = $result; + } + 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 + * @param string $name + * @param bool|integer $currentId + * @param bool|integer $bookId Only pass if type is not a book + * @return string + */ + public function findSuitableSlug($type, $name, $currentId = false, $bookId = false) + { + $slug = $this->nameToSlug($name); + while ($this->slugExists($type, $slug, $currentId, $bookId)) { + $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); + } + return $slug; + } + + /** + * Check if a slug already exists in the database. + * @param string $type + * @param string $slug + * @param bool|integer $currentId + * @param bool|integer $bookId + * @return bool + */ + protected function slugExists($type, $slug, $currentId = false, $bookId = false) + { + $query = $this->getEntity($type)->where('slug', '=', $slug); + if (strtolower($type) === 'page' || strtolower($type) === 'chapter') { + $query = $query->where('book_id', '=', $bookId); + } + if ($currentId) $query = $query->where('id', '!=', $currentId); + return $query->count() > 0; + } + /** * Updates entity restrictions from a request * @param $request @@ -260,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 @@ -282,6 +684,518 @@ 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; + } + + + /** + * Render the page for viewing, Parsing and performing features such as page transclusion. + * @param Page $page + * @return mixed|string + */ + public function renderPage(Page $page) + { + $content = $page->html; + $matches = []; + preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches); + if (count($matches[0]) === 0) return $content; + + foreach ($matches[1] as $index => $includeId) { + $splitInclude = explode('#', $includeId, 2); + $pageId = intval($splitInclude[0]); + if (is_nan($pageId)) continue; + + $page = $this->getById('page', $pageId); + if ($page === null) { + $content = str_replace($matches[0][$index], '', $content); + continue; + } + + if (count($splitInclude) === 1) { + $content = str_replace($matches[0][$index], $page->html, $content); + continue; + } + + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding('
'.$page->html.'', 'HTML-ENTITIES', 'UTF-8')); + $matchingElem = $doc->getElementById($splitInclude[1]); + if ($matchingElem === null) { + $content = str_replace($matches[0][$index], '', $content); + continue; + } + $innerContent = ''; + foreach ($matchingElem->childNodes as $childNode) { + $innerContent .= $doc->saveHTML($childNode); + } + $content = str_replace($matches[0][$index], trim($innerContent), $content); + } + + return $content; + } + + /** + * 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 String $pageContent + * @return array + */ + public function getPageNav($pageContent) + { + if ($pageContent == '') return []; + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8')); + $xPath = new DOMXPath($doc); + $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); + + if (is_null($headers)) return []; + + $tree = collect([]); + foreach ($headers as $header) { + $text = $header->nodeValue; + $tree->push([ + 'nodeName' => strtolower($header->nodeName), + 'level' => intval(str_replace('h', '', $header->nodeName)), + 'link' => '#' . $header->getAttribute('id'), + 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text + ]); + } + + // Normalise headers if only smaller headers have been used + if (count($tree) > 0) { + $minLevel = $tree->pluck('level')->min(); + $tree = $tree->map(function($header) use ($minLevel) { + $header['level'] -= ($minLevel - 2); + return $header; + }); + } + return $tree->toArray(); + } + + /** + * 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 e6d713f77..000000000 --- a/app/Repos/PageRepo.php +++ /dev/null @@ -1,666 +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; - } - - /** - * Get a page via a specific ID. - * @param $id - * @param bool $allowDrafts - * @return Page - */ - public function getById($id, $allowDrafts = false) - { - return $this->pageQuery($allowDrafts)->findOrFail($id); - } - - /** - * Get a page identified by the given slug. - * @param $slug - * @param $bookId - * @return Page - * @throws NotFoundException - */ - public function getBySlug($slug, $bookId) - { - $page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first(); - if ($page === null) throw new NotFoundException('Page not found'); - return $page; - } - - /** - * Search through page revisions and retrieve - * the last page in the current book that - * has a slug equal to the one given. - * @param $pageSlug - * @param $bookSlug - * @return null | Page - */ - public function findPageUsingOldSlug($pageSlug, $bookSlug) - { - $revision = $this->pageRevision->where('slug', '=', $pageSlug) - ->whereHas('page', function ($query) { - $this->permissionService->enforcePageRestrictions($query); - }) - ->where('type', '=', 'version') - ->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc') - ->with('page')->first(); - return $revision !== null ? $revision->page : null; - } - - /** - * Get a new Page instance from the given input. - * @param $input - * @return Page - */ - public function newFromInput($input) - { - $page = $this->page->fill($input); - return $page; - } - - /** - * 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($draftPage->name, $draftPage->book->id); - $draftPage->html = $this->formatHtml($input['html']); - $draftPage->text = strip_tags($draftPage->html); - $draftPage->draft = false; - - $draftPage->save(); - $this->saveRevision($draftPage, 'Initial Publish'); - - return $draftPage; - } - - /** - * Get a new draft page instance. - * @param Book $book - * @param Chapter|bool $chapter - * @return static - */ - public function getDraftPage(Book $book, $chapter = false) - { - $page = $this->page->newInstance(); - $page->name = 'New Page'; - $page->created_by = 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; - } - - - /** - * Gets pages by a search term. - * Highlights page content for showing in results. - * @param string $term - * @param array $whereTerms - * @param int $count - * @param array $paginationAppends - * @return mixed - */ - public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = []) - { - $terms = $this->prepareSearchTerms($term); - $pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms)); - $pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term); - $pages = $pageQuery->paginate($count)->appends($paginationAppends); - - // Add highlights to page text. - $words = join('|', explode(' ', preg_quote(trim($term), '/'))); - //lookahead/behind assertions ensures cut between words - $s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words - - foreach ($pages as $page) { - preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER); - //delimiter between occurrences - $results = []; - foreach ($matches as $line) { - $results[] = htmlspecialchars($line[0], 0, 'UTF-8'); - } - $matchLimit = 6; - if (count($results) > $matchLimit) { - $results = array_slice($results, 0, $matchLimit); - } - $result = join('... ', $results); - - //highlight - $result = preg_replace('#' . $words . '#iu', "\$0", $result); - if (strlen($result) < 5) { - $result = $page->getExcerpt(80); - } - $page->searchSnippet = $result; - } - return $pages; - } - - /** - * 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($input['name'], $book_id, $page->id); - } - - // Save page tags if present - if (isset($input['tags'])) { - $this->tagRepo->saveTagsToEntity($page, $input['tags']); - } - - // Update with new details - $userId = 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->name, $book->id, $page->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 = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.'; - if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) { - $message .= "\n This page has been updated by since that time. It is recommended that you discard this draft."; - } - return $message; - } - - /** - * Check if a page is being actively editing. - * Checks for edits since last page updated. - * Passing in a minuted range will check for edits - * within the last x minutes. - * @param Page $page - * @param null $minRange - * @return bool - */ - public function isPageEditingActive(Page $page, $minRange = null) - { - $draftSearch = $this->activePageEditingQuery($page, $minRange); - return $draftSearch->count() > 0; - } - - /** - * Get a notification message concerning the editing activity on - * a particular page. - * @param Page $page - * @param null $minRange - * @return string - */ - public function getPageEditingActiveMessage(Page $page, $minRange = null) - { - $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get(); - $userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has'; - $timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes'; - $message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!'; - return sprintf($message, $userMessage, $timeMessage); - } - - /** - * A query to check for active update drafts on a particular page. - * @param Page $page - * @param null $minRange - * @return mixed - */ - private function activePageEditingQuery(Page $page, $minRange = null) - { - $query = $this->pageRevision->where('type', '=', 'update_draft') - ->where('page_id', '=', $page->id) - ->where('updated_at', '>', $page->updated_at) - ->where('created_by', '!=', 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); - } - - /** - * Checks if a slug exists within a book already. - * @param $slug - * @param $bookId - * @param bool|false $currentId - * @return bool - */ - public function doesSlugExist($slug, $bookId, $currentId = false) - { - $query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId); - if ($currentId) $query = $query->where('id', '!=', $currentId); - return $query->count() > 0; - } - - /** - * 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->name, $bookId, $page->id); - $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); - } - - /** - * Gets a suitable slug for the resource - * @param string $name - * @param int $bookId - * @param bool|false $currentId - * @return string - */ - public function findSuitableSlug($name, $bookId, $currentId = false) - { - $slug = $this->nameToSlug($name); - while ($this->doesSlugExist($slug, $bookId, $currentId)) { - $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); - } - return $slug; - } - - /** - * 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/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index 24497c911..aa58d1718 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -93,7 +93,7 @@ class PermissionsRepo $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $this->assignRolePermissions($role, $permissions); - if ($role->name === 'admin') { + if ($role->system_name === 'admin') { $permissions = $this->permission->all()->pluck('id')->toArray(); $role->permissions()->sync($permissions); } @@ -133,9 +133,9 @@ class PermissionsRepo // Prevent deleting admin role or default registration role. if ($role->system_name && in_array($role->system_name, $this->systemRoles)) { - throw new PermissionsException('This role is a system role and cannot be deleted'); + throw new PermissionsException(trans('errors.role_system_cannot_be_deleted')); } else if ($role->id == setting('registration-role')) { - throw new PermissionsException('This role cannot be deleted while set as the default registration role.'); + throw new PermissionsException(trans('errors.role_registration_default_cannot_delete')); } if ($migrateRoleId) { diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index 6d0857f8b..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(); } @@ -121,7 +121,7 @@ class TagRepo /** * Create a new Tag instance from user input. * @param $input - * @return static + * @return Tag */ protected function newInstanceFromInput($input) { diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index ab3716fca..c3546a442 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -3,7 +3,6 @@ use BookStack\Role; use BookStack\User; use Exception; -use Setting; class UserRepo { @@ -169,13 +168,13 @@ class UserRepo public function getRecentlyCreated(User $user, $count = 20) { return [ - 'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) { + 'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) { $query->where('created_by', '=', $user->id); }), - 'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) { + 'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) { $query->where('created_by', '=', $user->id); }), - 'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) { + 'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) { $query->where('created_by', '=', $user->id); }) ]; diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index e41036238..2368ba10a 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -114,7 +114,7 @@ class ActivityService $activity = $this->permissionService ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type') - ->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); + ->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get(); return $this->filterSimilar($activity); } diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php index e0ee3a04d..592d67e63 100644 --- a/app/Services/AttachmentService.php +++ b/app/Services/AttachmentService.php @@ -193,7 +193,7 @@ class AttachmentService extends UploadService try { $storage->put($attachmentStoragePath, $attachmentData); } catch (Exception $e) { - throw new FileUploadException('File path ' . $attachmentStoragePath . ' could not be uploaded to. Ensure it is writable to the server.'); + throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath])); } return $attachmentPath; } diff --git a/app/Services/EmailConfirmationService.php b/app/Services/EmailConfirmationService.php index d4ec1e976..8eb52708c 100644 --- a/app/Services/EmailConfirmationService.php +++ b/app/Services/EmailConfirmationService.php @@ -33,7 +33,7 @@ class EmailConfirmationService public function sendConfirmation(User $user) { if ($user->email_confirmed) { - throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login'); + throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login'); } $this->deleteConfirmationsByUser($user); @@ -63,7 +63,7 @@ class EmailConfirmationService * Gets an email confirmation by looking up the token, * Ensures the token has not expired. * @param string $token - * @return EmailConfirmation + * @return array|null|\stdClass * @throws UserRegistrationException */ public function getEmailConfirmationFromToken($token) @@ -72,14 +72,14 @@ class EmailConfirmationService // If not found show error if ($emailConfirmation === null) { - throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register'); + throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register'); } // If more than a day old if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) { $user = $this->users->getById($emailConfirmation->user_id); $this->sendConfirmation($user); - throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm'); + throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm'); } $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id); diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 14084d320..880bc54ad 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -1,11 +1,22 @@ entityRepo = $entityRepo; + } + /** * Convert a page to a self-contained HTML file. * Includes required CSS & image content. Images are base64 encoded into the HTML. @@ -15,7 +26,7 @@ class ExportService public function pageToContainedHtml(Page $page) { $cssContent = file_get_contents(public_path('/css/export-styles.css')); - $pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render(); + $pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); return $this->containHtml($pageHtml); } @@ -27,9 +38,15 @@ class ExportService public function pageToPdf(Page $page) { $cssContent = file_get_contents(public_path('/css/export-styles.css')); - $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render(); + $pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render(); +// return $pageHtml; + $useWKHTML = config('snappy.pdf.binary') !== false; $containedHtml = $this->containHtml($pageHtml); - $pdf = \PDF::loadHTML($containedHtml); + if ($useWKHTML) { + $pdf = \SnappyPDF::loadHTML($containedHtml); + } else { + $pdf = \PDF::loadHTML($containedHtml); + } return $pdf->output(); } @@ -55,9 +72,13 @@ class ExportService $pathString = $srcString; } if ($isLocal && !file_exists($pathString)) continue; - $imageContent = file_get_contents($pathString); - $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); - $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); + try { + $imageContent = file_get_contents($pathString); + $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); + $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); + } catch (\ErrorException $e) { + $newImageString = ''; + } $htmlContent = str_replace($oldImgString, $newImageString, $htmlContent); } } @@ -84,14 +105,14 @@ class ExportService /** * Converts the page contents into simple plain text. - * This method filters any bad looking content to - * provide a nice final output. + * This method filters any bad looking content to provide a nice final output. * @param Page $page * @return mixed */ public function pageToPlainText(Page $page) { - $text = $page->text; + $html = $this->entityRepo->renderPage($page); + $text = strip_tags($html); // Replace multiple spaces with single spaces $text = preg_replace('/\ {2,}/', ' ', $text); // Reduce multiple horrid whitespace characters. diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index dfe2cf453..e34b3fb2b 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -59,7 +59,7 @@ class ImageService extends UploadService { $imageName = $imageName ? $imageName : basename($url); $imageData = file_get_contents($url); - if($imageData === false) throw new \Exception('Cannot get image from ' . $url); + if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); return $this->saveNew($imageName, $imageData, $type); } @@ -93,7 +93,7 @@ class ImageService extends UploadService $storage->put($fullPath, $imageData); $storage->setVisibility($fullPath, 'public'); } catch (Exception $e) { - throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); + throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath])); } if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath); @@ -160,7 +160,7 @@ class ImageService extends UploadService $thumb = $this->imageTool->make($storage->get($imagePath)); } catch (Exception $e) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { - throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); + throw new ImageUploadException(trans('errors.cannot_create_thumbs')); } else { throw $e; } diff --git a/app/Services/Ldap.php b/app/Services/Ldap.php index 196e46a2f..9c3bec327 100644 --- a/app/Services/Ldap.php +++ b/app/Services/Ldap.php @@ -94,4 +94,4 @@ class Ldap return ldap_bind($ldapConnection, $bindRdn, $bindPassword); } -} \ No newline at end of file +} diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php index b7f101ad2..f8a4b88bb 100644 --- a/app/Services/LdapService.php +++ b/app/Services/LdapService.php @@ -94,7 +94,7 @@ class LdapService $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); } - if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details')); + if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); } /** @@ -109,15 +109,19 @@ class LdapService // Check LDAP extension in installed if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { - throw new LdapException('LDAP PHP extension not installed'); + throw new LdapException(trans('errors.ldap_extension_not_installed')); } - // Get port from server string if specified. + // Get port from server string and protocol if specified. $ldapServer = explode(':', $this->config['server']); - $ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389); + $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1; + if (!$hasProtocol) array_unshift($ldapServer, ''); + $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1]; + $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389; + $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort); if ($ldapConnection === false) { - throw new LdapException('Cannot connect to ldap server, Initial connection failed'); + throw new LdapException(trans('errors.ldap_cannot_connect')); } // Set any required options diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index bb78f0b0a..39a2c38be 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -8,8 +8,9 @@ use BookStack\Ownable; use BookStack\Page; use BookStack\Role; use BookStack\User; +use Illuminate\Database\Connection; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Log; class PermissionService { @@ -23,6 +24,8 @@ class PermissionService public $chapter; public $page; + protected $db; + protected $jointPermission; protected $role; @@ -31,18 +34,21 @@ class PermissionService /** * PermissionService constructor. * @param JointPermission $jointPermission + * @param Connection $db * @param Book $book * @param Chapter $chapter * @param Page $page * @param Role $role */ - public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role) + public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role) { + $this->db = $db; $this->jointPermission = $jointPermission; $this->role = $role; $this->book = $book; $this->chapter = $chapter; $this->page = $page; + // TODO - Update so admin still goes through filters } /** @@ -151,7 +157,7 @@ class PermissionService */ public function buildJointPermissionsForEntity(Entity $entity) { - $roles = $this->role->with('jointPermissions')->get(); + $roles = $this->role->get(); $entities = collect([$entity]); if ($entity->isA('book')) { @@ -171,7 +177,7 @@ class PermissionService */ public function buildJointPermissionsForEntities(Collection $entities) { - $roles = $this->role->with('jointPermissions')->get(); + $roles = $this->role->get(); $this->deleteManyJointPermissionsForEntities($entities); $this->createManyJointPermissions($entities, $roles); } @@ -302,6 +308,10 @@ class PermissionService $explodedAction = explode('-', $action); $restrictionAction = end($explodedAction); + if ($role->system_name === 'admin') { + return $this->createJointPermissionDataArray($entity, $role, $action, true, true); + } + if ($entity->isA('book')) { if (!$entity->restricted) { @@ -395,7 +405,7 @@ class PermissionService $action = end($explodedPermission); $this->currentAction = $action; - $nonJointPermissions = ['restrictions']; + $nonJointPermissions = ['restrictions', 'image', 'attachment']; // Handle non entity specific jointPermissions if (in_array($explodedPermission[0], $nonJointPermissions)) { @@ -411,7 +421,6 @@ class PermissionService $this->currentAction = $permission; } - $q = $this->entityRestrictionQuery($baseQuery)->count() > 0; $this->clean(); return $q; @@ -462,60 +471,67 @@ class PermissionService } /** - * Add restrictions for a page query - * @param $query - * @param string $action - * @return mixed + * Get the children of a book in an efficient single query, Filtered by the permission system. + * @param integer $book_id + * @param bool $filterDrafts + * @return \Illuminate\Database\Query\Builder */ - public function enforcePageRestrictions($query, $action = 'view') - { - // Prevent drafts being visible to others. - $query = $query->where(function ($query) { - $query->where('draft', '=', false); - if ($this->currentUser()) { - $query->orWhere(function ($query) { - $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id); + public function bookChildrenQuery($book_id, $filterDrafts = false) { + $pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) { + $query->where('draft', '=', 0); + if (!$filterDrafts) { + $query->orWhere(function($query) { + $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id); }); } }); + $chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id); + $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) + ->mergeBindings($pageSelect)->mergeBindings($chapterSelect); - return $this->enforceEntityRestrictions($query, $action); - } + if (!$this->isAdmin()) { + $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)') + ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type') + ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles()) + ->where(function($query) { + $query->where('jp.has_permission', '=', 1)->orWhere(function($query) { + $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id); + }); + }); + $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery); + } - /** - * Add on permission restrictions to a chapter query. - * @param $query - * @param string $action - * @return mixed - */ - public function enforceChapterRestrictions($query, $action = 'view') - { - return $this->enforceEntityRestrictions($query, $action); - } - - /** - * Add restrictions to a book query. - * @param $query - * @param string $action - * @return mixed - */ - public function enforceBookRestrictions($query, $action = 'view') - { - return $this->enforceEntityRestrictions($query, $action); + $query->orderBy('draft', 'desc')->orderBy('priority', 'asc'); + $this->clean(); + return $query; } /** * Add restrictions for a generic entity - * @param $query + * @param string $entityType + * @param Builder|Entity $query * @param string $action * @return mixed */ - public function enforceEntityRestrictions($query, $action = 'view') + public function enforceEntityRestrictions($entityType, $query, $action = 'view') { + if (strtolower($entityType) === 'page') { + // Prevent drafts being visible to others. + $query = $query->where(function ($query) { + $query->where('draft', '=', false); + if ($this->currentUser()) { + $query->orWhere(function ($query) { + $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id); + }); + } + }); + } + if ($this->isAdmin()) { $this->clean(); return $query; } + $this->currentAction = $action; return $this->entityRestrictionQuery($query); } @@ -553,6 +569,7 @@ class PermissionService }); }); }); + $this->clean(); return $q; } @@ -601,7 +618,7 @@ class PermissionService private function isAdmin() { if ($this->isAdminUser === null) { - $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false; + $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false; } return $this->isAdminUser; diff --git a/app/Services/SettingService.php b/app/Services/SettingService.php index bf5fa918e..40094a513 100644 --- a/app/Services/SettingService.php +++ b/app/Services/SettingService.php @@ -1,6 +1,7 @@ getValueFromStore($key, $default); return $this->formatValue($value, $default); } + /** + * Get a user-specific setting from the database or cache. + * @param User $user + * @param $key + * @param bool $default + * @return bool|string + */ + public function getUser($user, $key, $default = false) + { + return $this->get($this->userKey($user->id, $key), $default); + } + /** * Gets a setting value from the cache or database. * Looks at the system defaults if not cached or in database. @@ -69,14 +83,6 @@ class SettingService return $value; } - // Check the defaults set in the app config. - $configPrefix = 'setting-defaults.' . $key; - if (config()->has($configPrefix)) { - $value = config($configPrefix); - $this->cache->forever($cacheKey, $value); - return $value; - } - return $default; } @@ -118,6 +124,16 @@ class SettingService return $setting !== null; } + /** + * Check if a user setting is in the database. + * @param $key + * @return bool + */ + public function hasUser($key) + { + return $this->has($this->userKey($key)); + } + /** * Add a setting to the database. * @param $key @@ -135,6 +151,28 @@ class SettingService return true; } + /** + * Put a user-specific setting into the database. + * @param User $user + * @param $key + * @param $value + * @return bool + */ + public function putUser($user, $key, $value) + { + return $this->put($this->userKey($user->id, $key), $value); + } + + /** + * Convert a setting key into a user-specific key. + * @param $key + * @return string + */ + protected function userKey($userId, $key = '') + { + return 'user:' . $userId . ':' . $key; + } + /** * Removes a setting from the database. * @param $key @@ -150,6 +188,16 @@ class SettingService return true; } + /** + * Delete settings for a given user id. + * @param $userId + * @return mixed + */ + public function deleteUserSettings($userId) + { + return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete(); + } + /** * Gets a setting model from the database for the given key. * @param $key diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php index d76a7231b..5edd4cad7 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Services/SocialAuthService.php @@ -70,12 +70,12 @@ class SocialAuthService // Check social account has not already been used if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) { - throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login'); + throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login'); } if ($this->userRepo->getByEmail($socialUser->getEmail())) { $email = $socialUser->getEmail(); - throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login'); + throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login'); } return $socialUser; @@ -98,7 +98,6 @@ class SocialAuthService // Get any attached social accounts or users $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first(); - $user = $this->userRepo->getByEmail($socialUser->getEmail()); $isLoggedIn = auth()->check(); $currentUser = user(); @@ -113,27 +112,26 @@ class SocialAuthService if ($isLoggedIn && $socialAccount === null) { $this->fillSocialAccount($socialDriver, $socialUser); $currentUser->socialAccounts()->save($this->socialAccount); - session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.'); + session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)])); return redirect($currentUser->getEditUrl()); } // When a user is logged in and the social account exists and is already linked to the current user. if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { - session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.'); + session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)])); return redirect($currentUser->getEditUrl()); } // When a user is logged in, A social account exists but the users do not match. - // Change the user that the social account is assigned to. if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { - session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.'); + session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)])); return redirect($currentUser->getEditUrl()); } // Otherwise let the user know this social account is not used by anyone. - $message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings'; + $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]); if (setting('registration-enabled')) { - $message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option'; + $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]); } throw new SocialSignInException($message . '.', '/login'); @@ -157,8 +155,8 @@ class SocialAuthService { $driver = trim(strtolower($socialDriver)); - if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found'); - if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly."); + if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found')); + if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)])); return $driver; } @@ -215,7 +213,7 @@ class SocialAuthService { session(); user()->socialAccounts()->where('driver', '=', $socialDriver)->delete(); - session()->flash('success', title_case($socialDriver) . ' account successfully detached'); + session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)])); return redirect(user()->getEditUrl()); } diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index 1a9ee5f70..3285745ce 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -5,9 +5,7 @@ use BookStack\View; class ViewService { - protected $view; - protected $user; protected $permissionService; /** @@ -18,7 +16,6 @@ class ViewService public function __construct(View $view, PermissionService $permissionService) { $this->view = $view; - $this->user = user(); $this->permissionService = $permissionService; } @@ -29,8 +26,9 @@ class ViewService */ public function add(Entity $entity) { - if ($this->user === null) return 0; - $view = $entity->views()->where('user_id', '=', $this->user->id)->first(); + $user = user(); + if ($user === null || $user->isDefault()) return 0; + $view = $entity->views()->where('user_id', '=', $user->id)->first(); // Add view if model exists if ($view) { $view->increment('views'); @@ -39,7 +37,7 @@ class ViewService // Otherwise create new view count $entity->views()->save($this->view->create([ - 'user_id' => $this->user->id, + 'user_id' => $user->id, 'views' => 1 ])); @@ -78,13 +76,14 @@ class ViewService */ public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) { - if ($this->user === null) return collect(); + $user = user(); + if ($user === null || $user->isDefault()) return collect(); $query = $this->permissionService ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); - $query = $query->where('user_id', '=', user()->id); + $query = $query->where('user_id', '=', $user->id); $viewables = $query->with('viewable')->orderBy('updated_at', 'desc') ->skip($count * $page)->take($count)->get()->pluck('viewable'); diff --git a/app/User.php b/app/User.php index 09b189cbb..afcd9af70 100644 --- a/app/User.php +++ b/app/User.php @@ -74,6 +74,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->roles->pluck('name')->contains($role); } + /** + * Check if the user has a role. + * @param $role + * @return mixed + */ + public function hasSystemRole($role) + { + return $this->roles->pluck('system_name')->contains('admin'); + } + /** * Get all permissions belonging to a the current user. * @param bool $cache @@ -150,8 +160,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function getAvatar($size = 50) { - if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return baseUrl('/user_avatar.png'); - return baseUrl($this->avatar->getThumb($size, $size, false)); + $default = baseUrl('/user_avatar.png'); + $imageId = $this->image_id; + if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default; + + try { + $avatar = baseUrl($this->avatar->getThumb($size, $size, false)); + } catch (\Exception $err) { + $avatar = $default; + } + return $avatar; } /** diff --git a/app/helpers.php b/app/helpers.php index b5be0fd11..6decb08e9 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -60,11 +60,12 @@ function userCan($permission, Ownable $ownable = null) * Helper to access system settings. * @param $key * @param bool $default - * @return mixed + * @return bool|string|\BookStack\Services\SettingService */ -function setting($key, $default = false) +function setting($key = null, $default = false) { $settingService = app(\BookStack\Services\SettingService::class); + if (is_null($key)) return $settingService; return $settingService->get($key, $default); } diff --git a/composer.json b/composer.json index 7d4b5e62b..5a8fd67ae 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "league/flysystem-aws-s3-v3": "^1.0", "barryvdh/laravel-dompdf": "^0.7", "predis/predis": "^1.1", - "gathercontent/htmldiff": "^0.2.1" + "gathercontent/htmldiff": "^0.2.1", + "barryvdh/laravel-snappy": "^0.3.1" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/composer.lock b/composer.lock index 74a090288..dcde9d9c6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "3124d900cfe857392a94de479f3ff6d4", - "content-hash": "a968767a73f77e66e865c276cf76eedf", + "hash": "2438a2f4a02adbea5f378f9e9408eb29", + "content-hash": "6add8bff71ecc86e0c90858590834a26", "packages": [ { "name": "aws/aws-sdk-php", @@ -255,6 +255,58 @@ ], "time": "2016-07-04 11:52:48" }, + { + "name": "barryvdh/laravel-snappy", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-snappy.git", + "reference": "509a4497be63d8ee7ff464a3daf00d9edde08e21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/509a4497be63d8ee7ff464a3daf00d9edde08e21", + "reference": "509a4497be63d8ee7ff464a3daf00d9edde08e21", + "shasum": "" + }, + "require": { + "illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x", + "illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x", + "knplabs/knp-snappy": "*", + "php": ">=5.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Snappy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Snappy PDF/Image for Laravel 4", + "keywords": [ + "image", + "laravel", + "pdf", + "snappy", + "wkhtmltoimage", + "wkhtmltopdf" + ], + "time": "2016-08-05 13:08:28" + }, { "name": "barryvdh/reflection-docblock", "version": "v2.0.4", @@ -997,6 +1049,71 @@ ], "time": "2015-12-05 17:17:57" }, + { + "name": "knplabs/knp-snappy", + "version": "0.4.3", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/snappy.git", + "reference": "44f7a9b37d5686fd7db4c1e9569a802a5d16923f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/44f7a9b37d5686fd7db4c1e9569a802a5d16923f", + "reference": "44f7a9b37d5686fd7db4c1e9569a802a5d16923f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/process": "~2.3|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.7" + }, + "suggest": { + "h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency", + "h4cc/wkhtmltoimage-i386": "Provides wkhtmltoimage-i386 binary for Linux-compatible machines, use version `~0.12` as dependency", + "h4cc/wkhtmltopdf-amd64": "Provides wkhtmltopdf-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency", + "h4cc/wkhtmltopdf-i386": "Provides wkhtmltopdf-i386 binary for Linux-compatible machines, use version `~0.12` as dependency", + "wemersonjanuario/wkhtmltopdf-windows": "Provides wkhtmltopdf executable for Windows, use version `~0.12` as dependency" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Knp\\Snappy": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KNPLabs Team", + "homepage": "http://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "http://github.com/KnpLabs/snappy/contributors" + } + ], + "description": "PHP5 library allowing thumbnail, snapshot or PDF generation from a url or a html page. Wrapper for wkhtmltopdf/wkhtmltoimage.", + "homepage": "http://github.com/KnpLabs/snappy", + "keywords": [ + "knp", + "knplabs", + "pdf", + "snapshot", + "thumbnail", + "wkhtmltopdf" + ], + "time": "2015-11-17 13:16:27" + }, { "name": "laravel/framework", "version": "v5.3.11", diff --git a/config/app.php b/config/app.php index 786f005ac..0c3e1e71c 100644 --- a/config/app.php +++ b/config/app.php @@ -148,6 +148,7 @@ return [ Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, Barryvdh\Debugbar\ServiceProvider::class, + Barryvdh\Snappy\ServiceProvider::class, /* @@ -218,6 +219,7 @@ return [ 'ImageTool' => Intervention\Image\Facades\Image::class, 'PDF' => Barryvdh\DomPDF\Facade::class, + 'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class, 'Debugbar' => Barryvdh\Debugbar\Facade::class, /** diff --git a/config/setting-defaults.php b/config/setting-defaults.php index c681bb7f5..db35023d5 100644 --- a/config/setting-defaults.php +++ b/config/setting-defaults.php @@ -6,6 +6,7 @@ return [ 'app-name' => 'BookStack', + 'app-logo' => '', 'app-name-header' => true, 'app-editor' => 'wysiwyg', 'app-color' => '#0288D1', diff --git a/config/snappy.php b/config/snappy.php new file mode 100644 index 000000000..73f21fd30 --- /dev/null +++ b/config/snappy.php @@ -0,0 +1,18 @@ + [ + 'enabled' => true, + 'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false), + 'timeout' => false, + 'options' => [], + 'env' => [], + ], + 'image' => [ + 'enabled' => false, + 'binary' => '/usr/local/bin/wkhtmltoimage', + 'timeout' => false, + 'options' => [], + 'env' => [], + ], +]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 3820d5b59..43e214386 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -59,4 +59,14 @@ $factory->define(BookStack\Tag::class, function ($faker) { 'name' => $faker->city, 'value' => $faker->sentence(3) ]; +}); + +$factory->define(BookStack\Image::class, function ($faker) { + return [ + 'name' => $faker->slug . '.jpg', + 'url' => $faker->url, + 'path' => $faker->url, + 'type' => 'gallery', + 'uploaded_to' => 0 + ]; }); \ No newline at end of file diff --git a/database/migrations/2017_01_21_163556_create_cache_table.php b/database/migrations/2017_01_21_163556_create_cache_table.php new file mode 100644 index 000000000..1f7761c2b --- /dev/null +++ b/database/migrations/2017_01_21_163556_create_cache_table.php @@ -0,0 +1,32 @@ +string('key')->unique(); + $table->text('value'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('cache'); + } +} diff --git a/database/migrations/2017_01_21_163602_create_sessions_table.php b/database/migrations/2017_01_21_163602_create_sessions_table.php new file mode 100644 index 000000000..56e76d6df --- /dev/null +++ b/database/migrations/2017_01_21_163602_create_sessions_table.php @@ -0,0 +1,35 @@ +string('id')->unique(); + $table->integer('user_id')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->text('payload'); + $table->integer('last_activity'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('sessions'); + } +} diff --git a/package.json b/package.json index 30f288d45..b0805c918 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "private": true, "scripts": { - "prod": "gulp --production", - "dev": "gulp watch" + "build": "gulp --production", + "dev": "gulp watch", + "watch": "gulp watch" }, "devDependencies": { "angular": "^1.5.5", @@ -15,7 +16,9 @@ "laravel-elixir": "^6.0.0-11", "laravel-elixir-browserify-official": "^0.1.3", "marked": "^0.3.5", - "moment": "^2.12.0", - "zeroclipboard": "^2.2.0" + "moment": "^2.12.0" + }, + "dependencies": { + "clipboard": "^1.5.16" } } diff --git a/phpunit.xml b/phpunit.xml index 72e06a3fc..2e07cdbf8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,6 +22,7 @@Enter your email below and you will be sent an email with a password reset link.
+{{ trans('auth.reset_password_send_instructions') }}
Please check your email and click the confirmation button to access {{ setting('app-name', 'BookStack') }}.
+{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}
Register and sign in using another service.
+{{ trans('auth.social_registration_text') }}
@if(isset($socialDrivers['google'])) @endif diff --git a/resources/views/auth/user-unconfirmed.blade.php b/resources/views/auth/user-unconfirmed.blade.php index 08178e891..13567b412 100644 --- a/resources/views/auth/user-unconfirmed.blade.php +++ b/resources/views/auth/user-unconfirmed.blade.php @@ -4,16 +4,16 @@Your email address has not yet been confirmed.
- Please click the link in the email that was sent shortly after you registered.
- If you cannot find the email you can re-send the confirmation email by submitting the form below.
+
{{ trans('auth.email_not_confirmed_text') }}
+ {{ trans('auth.email_not_confirmed_click_link') }}
+ {{ trans('auth.email_not_confirmed_resend') }}
This will delete the book with the name '{{$book->name}}', All pages and chapters will be removed.
-Are you sure you want to delete this book?
+{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}
+{{ trans('entities.books_delete_confirmation') }}