diff --git a/app/Book.php b/app/Book.php index 51ea226b4..4e944ce10 100644 --- a/app/Book.php +++ b/app/Book.php @@ -48,14 +48,6 @@ class Book extends Entity { return $this->belongsTo(Image::class, 'image_id'); } - /* - * Get the edit url for this book. - * @return string - */ - public function getEditUrl() - { - return $this->getUrl() . '/edit'; - } /** * Get all pages within this book. @@ -75,6 +67,15 @@ class Book extends Entity return $this->hasMany(Chapter::class); } + /** + * Get the shelves this book is contained within. + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function shelves() + { + return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id'); + } + /** * Get an excerpt of this book's description to the specified length or less. * @param int $length diff --git a/app/Bookshelf.php b/app/Bookshelf.php new file mode 100644 index 000000000..9468575c3 --- /dev/null +++ b/app/Bookshelf.php @@ -0,0 +1,83 @@ +belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc'); + } + + /** + * Get the url for this bookshelf. + * @param string|bool $path + * @return string + */ + public function getUrl($path = false) + { + if ($path !== false) { + return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/')); + } + return baseUrl('/shelves/' . urlencode($this->slug)); + } + + /** + * Returns BookShelf cover image, if cover does not exists return default cover image. + * @param int $width - Width of the image + * @param int $height - Height of the image + * @return string + */ + public function getBookCover($width = 440, $height = 250) + { + $default = baseUrl('/book_default_cover.png'); + if (!$this->image_id) { + return $default; + } + + try { + $cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default; + } catch (\Exception $err) { + $cover = $default; + } + return $cover; + } + + /** + * Get the cover image of the book + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function cover() + { + return $this->belongsTo(Image::class, 'image_id'); + } + + /** + * Get an excerpt of this book's description to the specified length or less. + * @param int $length + * @return string + */ + public function getExcerpt($length = 100) + { + $description = $this->description; + return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; + } + + /** + * Return a generalised, common raw query that can be 'unioned' across entities. + * @return string + */ + public function entityRawQuery() + { + return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at"; + } +} diff --git a/app/Console/Commands/CleanupImages.php b/app/Console/Commands/CleanupImages.php index e05508d5e..8e1539702 100644 --- a/app/Console/Commands/CleanupImages.php +++ b/app/Console/Commands/CleanupImages.php @@ -72,7 +72,9 @@ class CleanupImages extends Command protected function showDeletedImages($paths) { - if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return; + if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) { + return; + } if (count($paths) > 0) { $this->line('Images to delete:'); } diff --git a/app/Entity.php b/app/Entity.php index 5d4449f2b..252a3e07a 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -152,7 +152,7 @@ class Entity extends Ownable */ public static function getEntityInstance($type) { - $types = ['Page', 'Book', 'Chapter']; + $types = ['Page', 'Book', 'Chapter', 'Bookshelf']; $className = str_replace([' ', '-', '_'], '', ucwords($type)); if (!in_array($className, $types)) { return null; @@ -168,10 +168,10 @@ class Entity extends Ownable */ public function getShortName($length = 25) { - if (strlen($this->name) <= $length) { + if (mb_strlen($this->name) <= $length) { return $this->name; } - return substr($this->name, 0, $length - 3) . '...'; + return mb_substr($this->name, 0, $length - 3) . '...'; } /** diff --git a/app/Exceptions/SocialSignInAccountNotUsed.php b/app/Exceptions/SocialSignInAccountNotUsed.php new file mode 100644 index 000000000..7eaa72bd5 --- /dev/null +++ b/app/Exceptions/SocialSignInAccountNotUsed.php @@ -0,0 +1,6 @@ +attachmentService->getAttachmentFromStorage($attachment); - return response($attachmentContents, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"' - ]); + return $this->downloadResponse($attachmentContents, $attachment->getFileName()); } /** diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 1bbd5e2ba..385994324 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -2,7 +2,7 @@ namespace BookStack\Http\Controllers\Auth; -use BookStack\Exceptions\ConfirmationEmailException; +use BookStack\Exceptions\SocialSignInAccountNotUsed; use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\UserRegistrationException; use BookStack\Repos\UserRepo; @@ -16,6 +16,7 @@ use Illuminate\Http\Response; use Validator; use BookStack\Http\Controllers\Controller; use Illuminate\Foundation\Auth\RegistersUsers; +use Laravel\Socialite\Contracts\User as SocialUser; class RegisterController extends Controller { @@ -133,25 +134,28 @@ class RegisterController extends Controller * The registrations flow for all users. * @param array $userData * @param bool|false|SocialAccount $socialAccount + * @param bool $emailVerified * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException */ - protected function registerUser(array $userData, $socialAccount = false) + protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false) { - if (setting('registration-restrict')) { - $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict'))); + $registrationRestrict = setting('registration-restrict'); + + if ($registrationRestrict) { + $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict)); $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1); if (!in_array($userEmailDomain, $restrictedEmailDomains)) { throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register'); } } - $newUser = $this->userRepo->registerNew($userData); + $newUser = $this->userRepo->registerNew($userData, $emailVerified); if ($socialAccount) { $newUser->socialAccounts()->save($socialAccount); } - if (setting('registration-confirmation') || setting('registration-restrict')) { + if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) { $newUser->save(); try { @@ -250,7 +254,6 @@ class RegisterController extends Controller * @throws SocialSignInException * @throws UserRegistrationException * @throws \BookStack\Exceptions\SocialDriverNotConfigured - * @throws ConfirmationEmailException */ public function socialCallback($socialDriver, Request $request) { @@ -267,12 +270,24 @@ class RegisterController extends Controller } $action = session()->pull('social-callback'); + + // Attempt login or fall-back to register if allowed. + $socialUser = $this->socialAuthService->getSocialUser($socialDriver); if ($action == 'login') { - return $this->socialAuthService->handleLoginCallback($socialDriver); + try { + return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser); + } catch (SocialSignInAccountNotUsed $exception) { + if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) { + return $this->socialRegisterCallback($socialDriver, $socialUser); + } + throw $exception; + } } + if ($action == 'register') { - return $this->socialRegisterCallback($socialDriver); + return $this->socialRegisterCallback($socialDriver, $socialUser); } + return redirect()->back(); } @@ -288,15 +303,16 @@ class RegisterController extends Controller /** * Register a new user after a registration callback. - * @param $socialDriver + * @param string $socialDriver + * @param SocialUser $socialUser * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException - * @throws \BookStack\Exceptions\SocialDriverNotConfigured */ - protected function socialRegisterCallback($socialDriver) + protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser) { - $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver); + $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser); $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser); + $emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver); // Create an array of the user data to create a new user instance $userData = [ @@ -304,6 +320,6 @@ class RegisterController extends Controller 'email' => $socialUser->getEmail(), 'password' => str_random(30) ]; - return $this->registerUser($userData, $socialAccount); + return $this->registerUser($userData, $socialAccount, $emailVerified); } } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 2c3946239..ea39a771e 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -299,10 +299,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $bookSlug); $pdfContent = $this->exportService->bookToPdf($book); - return response()->make($pdfContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf' - ]); + return $this->downloadResponse($pdfContent, $bookSlug . '.pdf'); } /** @@ -314,10 +311,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $bookSlug); $htmlContent = $this->exportService->bookToContainedHtml($book); - return response()->make($htmlContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html' - ]); + return $this->downloadResponse($htmlContent, $bookSlug . '.html'); } /** @@ -328,10 +322,7 @@ class BookController extends Controller public function exportPlainText($bookSlug) { $book = $this->entityRepo->getBySlug('book', $bookSlug); - $htmlContent = $this->exportService->bookToPlainText($book); - return response()->make($htmlContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt' - ]); + $textContent = $this->exportService->bookToPlainText($book); + return $this->downloadResponse($textContent, $bookSlug . '.txt'); } } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php new file mode 100644 index 000000000..8c7f7819e --- /dev/null +++ b/app/Http/Controllers/BookshelfController.php @@ -0,0 +1,243 @@ +entityRepo = $entityRepo; + $this->userRepo = $userRepo; + $this->exportService = $exportService; + parent::__construct(); + } + + /** + * Display a listing of the book. + * @return Response + */ + public function index() + { + $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); + $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false; + $popular = $this->entityRepo->getPopular('bookshelf', 4, 0); + $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0); + $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); + + $this->setPageTitle(trans('entities.shelves')); + return view('shelves/index', [ + 'shelves' => $shelves, + 'recents' => $recents, + 'popular' => $popular, + 'new' => $new, + 'shelvesViewType' => $shelvesViewType + ]); + } + + /** + * Show the form for creating a new bookshelf. + * @return Response + */ + public function create() + { + $this->checkPermission('bookshelf-create-all'); + $books = $this->entityRepo->getAll('book', false, 'update'); + $this->setPageTitle(trans('entities.shelves_create')); + return view('shelves/create', ['books' => $books]); + } + + /** + * Store a newly created bookshelf in storage. + * @param Request $request + * @return Response + */ + public function store(Request $request) + { + $this->checkPermission('bookshelf-create-all'); + $this->validate($request, [ + 'name' => 'required|string|max:255', + 'description' => 'string|max:1000', + ]); + + $bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); + $this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', '')); + Activity::add($bookshelf, 'bookshelf_create'); + + return redirect($bookshelf->getUrl()); + } + + + /** + * Display the specified bookshelf. + * @param String $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + */ + public function show(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('book-view', $bookshelf); + + $books = $this->entityRepo->getBookshelfChildren($bookshelf); + Views::add($bookshelf); + + $this->setPageTitle($bookshelf->getShortName()); + return view('shelves/show', [ + 'shelf' => $bookshelf, + 'books' => $books, + 'activity' => Activity::entityActivity($bookshelf, 20, 0) + ]); + } + + /** + * Show the form for editing the specified bookshelf. + * @param $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + */ + public function edit(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-update', $bookshelf); + + $shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf); + $shelfBookIds = $shelfBooks->pluck('id'); + $books = $this->entityRepo->getAll('book', false, 'update'); + $books = $books->filter(function ($book) use ($shelfBookIds) { + return !$shelfBookIds->contains($book->id); + }); + + $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()])); + return view('shelves/edit', [ + 'shelf' => $bookshelf, + 'books' => $books, + 'shelfBooks' => $shelfBooks, + ]); + } + + + /** + * Update the specified bookshelf in storage. + * @param Request $request + * @param string $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + */ + public function update(Request $request, string $slug) + { + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-update', $shelf); + $this->validate($request, [ + 'name' => 'required|string|max:255', + 'description' => 'string|max:1000', + ]); + + $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all()); + $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); + Activity::add($shelf, 'bookshelf_update'); + + return redirect($shelf->getUrl()); + } + + + /** + * Shows the page to confirm deletion + * @param $slug + * @return \Illuminate\View\View + * @throws \BookStack\Exceptions\NotFoundException + */ + public function showDelete(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $bookshelf); + + $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()])); + return view('shelves/delete', ['shelf' => $bookshelf]); + } + + /** + * Remove the specified bookshelf from storage. + * @param string $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + * @throws \Throwable + */ + public function destroy(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $bookshelf); + Activity::addMessage('bookshelf_delete', 0, $bookshelf->name); + $this->entityRepo->destroyBookshelf($bookshelf); + return redirect('/shelves'); + } + + /** + * Show the Restrictions view. + * @param $slug + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \BookStack\Exceptions\NotFoundException + */ + public function showRestrict(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $bookshelf); + + $roles = $this->userRepo->getRestrictableRoles(); + return view('shelves.restrictions', [ + 'shelf' => $bookshelf, + 'roles' => $roles + ]); + } + + /** + * Set the restrictions for this bookshelf. + * @param $slug + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws \BookStack\Exceptions\NotFoundException + */ + public function restrict(string $slug, Request $request) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $bookshelf); + + $this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf); + session()->flash('success', trans('entities.shelves_permissions_updated')); + return redirect($bookshelf->getUrl()); + } + + /** + * Copy the permissions of a bookshelf to the child books. + * @param string $slug + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws \BookStack\Exceptions\NotFoundException + */ + public function copyPermissions(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $bookshelf); + + $updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf); + session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount])); + return redirect($bookshelf->getUrl()); + } +} diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index b737afc6d..1fe231a65 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -250,10 +250,7 @@ class ChapterController extends Controller { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $pdfContent = $this->exportService->chapterToPdf($chapter); - return response()->make($pdfContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf' - ]); + return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf'); } /** @@ -266,10 +263,7 @@ class ChapterController extends Controller { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $containedHtml = $this->exportService->chapterToContainedHtml($chapter); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html' - ]); + return $this->downloadResponse($containedHtml, $chapterSlug . '.html'); } /** @@ -281,10 +275,7 @@ class ChapterController extends Controller public function exportPlainText($bookSlug, $chapterSlug) { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); - $containedHtml = $this->exportService->chapterToPlainText($chapter); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt' - ]); + $chapterText = $this->exportService->chapterToPlainText($chapter); + return $this->downloadResponse($chapterText, $chapterSlug . '.txt'); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index a51ff5d77..33b57b7d9 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -136,7 +136,6 @@ abstract class Controller extends BaseController /** * Create the response for when a request fails validation. - * * @param \Illuminate\Http\Request $request * @param array $errors * @return \Symfony\Component\HttpFoundation\Response @@ -151,4 +150,18 @@ abstract class Controller extends BaseController ->withInput($request->input()) ->withErrors($errors, $this->errorBag()); } + + /** + * Create a response that forces a download in the browser. + * @param string $content + * @param string $fileName + * @return \Illuminate\Http\Response + */ + protected function downloadResponse(string $content, string $fileName) + { + return response()->make($content, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="' . $fileName . '"' + ]); + } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 2077f6888..e47250318 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -33,42 +33,42 @@ class HomeController extends Controller $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12); - - $customHomepage = false; - $books = false; - $booksViewType = false; - - // Check book homepage - $bookHomepageSetting = setting('app-book-homepage'); - if ($bookHomepageSetting) { - $books = $this->entityRepo->getAllPaginated('book', 18); - $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); - } else { - // Check custom homepage - $homepageSetting = setting('app-homepage'); - if ($homepageSetting) { - $id = intval(explode(':', $homepageSetting)[0]); - $customHomepage = $this->entityRepo->getById('page', $id, false, true); - $this->entityRepo->renderPage($customHomepage, true); - } + $homepageOptions = ['default', 'books', 'bookshelves', 'page']; + $homepageOption = setting('app-homepage-type', 'default'); + if (!in_array($homepageOption, $homepageOptions)) { + $homepageOption = 'default'; } - $view = 'home'; - if ($bookHomepageSetting) { - $view = 'home-book'; - } else if ($customHomepage) { - $view = 'home-custom'; - } - - return view('common/' . $view, [ + $commonData = [ 'activity' => $activity, 'recents' => $recents, 'recentlyUpdatedPages' => $recentlyUpdatedPages, 'draftPages' => $draftPages, - 'customHomepage' => $customHomepage, - 'books' => $books, - 'booksViewType' => $booksViewType - ]); + ]; + + if ($homepageOption === 'bookshelves') { + $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); + $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); + $data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]); + return view('common.home-shelves', $data); + } + + if ($homepageOption === 'books') { + $books = $this->entityRepo->getAllPaginated('book', 18); + $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); + $data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]); + return view('common.home-book', $data); + } + + if ($homepageOption === 'page') { + $homepageSetting = setting('app-homepage', '0:'); + $id = intval(explode(':', $homepageSetting)[0]); + $customHomepage = $this->entityRepo->getById('page', $id, false, true); + $this->entityRepo->renderPage($customHomepage, true); + return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage])); + } + + return view('common.home', $commonData); } /** diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 25a0503eb..80bbe56c1 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -454,6 +454,40 @@ class PageController extends Controller return redirect($page->getUrl()); } + + /** + * Deletes a revision using the id of the specified revision. + * @param string $bookSlug + * @param string $pageSlug + * @param int $revId + * @throws NotFoundException + * @throws BadRequestException + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function destroyRevision($bookSlug, $pageSlug, $revId) + { + $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $this->checkOwnablePermission('page-delete', $page); + + $revision = $page->revisions()->where('id', '=', $revId)->first(); + if ($revision === null) { + throw new NotFoundException("Revision #{$revId} not found"); + } + + // Get the current revision for the page + $currentRevision = $page->getCurrentRevision(); + + // Check if its the latest revision, cannot delete latest revision. + if (intval($currentRevision->id) === intval($revId)) { + session()->flash('error', trans('entities.revision_cannot_delete_latest')); + return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400); + } + + $revision->delete(); + session()->flash('success', trans('entities.revision_delete_success')); + return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); + } + /** * Exports a page to a PDF. * https://github.com/barryvdh/laravel-dompdf @@ -466,10 +500,7 @@ class PageController extends Controller $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page->html = $this->entityRepo->renderPage($page); $pdfContent = $this->exportService->pageToPdf($page); - return response()->make($pdfContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' - ]); + return $this->downloadResponse($pdfContent, $pageSlug . '.pdf'); } /** @@ -483,10 +514,7 @@ class PageController extends Controller $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); $page->html = $this->entityRepo->renderPage($page); $containedHtml = $this->exportService->pageToContainedHtml($page); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html' - ]); + return $this->downloadResponse($containedHtml, $pageSlug . '.html'); } /** @@ -498,11 +526,8 @@ class PageController extends Controller public function exportPlainText($bookSlug, $pageSlug) { $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); - $containedHtml = $this->exportService->pageToPlainText($page); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt' - ]); + $pageText = $this->exportService->pageToPlainText($page); + return $this->downloadResponse($pageText, $pageSlug . '.txt'); } /** diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d50baa86f..f6bd13e6f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -252,7 +252,7 @@ class UserController extends Controller return $this->currentUser->id == $id; }); - $viewType = $request->get('book_view_type'); + $viewType = $request->get('view_type'); if (!in_array($viewType, ['grid', 'list'])) { $viewType = 'list'; } @@ -262,4 +262,27 @@ class UserController extends Controller return redirect()->back(302, [], "/settings/users/$id"); } + + /** + * Update the user's preferred shelf-list display setting. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function switchShelfView($id, Request $request) + { + $this->checkPermissionOr('users-manage', function () use ($id) { + return $this->currentUser->id == $id; + }); + + $viewType = $request->get('view_type'); + if (!in_array($viewType, ['grid', 'list'])) { + $viewType = 'list'; + } + + $user = $this->user->findOrFail($id); + setting()->putUser($user, 'bookshelves_view_type', $viewType); + + return redirect()->back(302, [], "/settings/users/$id"); + } } diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php index bdbf6ccbd..528ff4047 100644 --- a/app/Http/Middleware/Localization.php +++ b/app/Http/Middleware/Localization.php @@ -6,6 +6,9 @@ use Illuminate\Http\Request; class Localization { + + protected $rtlLocales = ['ar']; + /** * Handle an incoming request. * @@ -23,6 +26,11 @@ class Localization $locale = setting()->getUser(user(), 'language', $defaultLang); } + // Set text direction + if (in_array($locale, $this->rtlLocales)) { + config()->set('app.rtl', true); + } + app()->setLocale($locale); Carbon::setLocale($locale); return $next($request); diff --git a/app/Image.php b/app/Image.php index 412beea90..acc82df90 100644 --- a/app/Image.php +++ b/app/Image.php @@ -19,5 +19,4 @@ class Image extends Ownable { return Images::getThumbnail($this, $width, $height, $keepRatio); } - } diff --git a/app/Page.php b/app/Page.php index 9554504b3..5c03e7d66 100644 --- a/app/Page.php +++ b/app/Page.php @@ -112,4 +112,13 @@ class Page extends Entity $htmlQuery = $withContent ? 'html' : "'' as html"; return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at"; } + + /** + * Get the current revision for the page if existing + * @return \BookStack\PageRevision|null + */ + public function getCurrentRevision() + { + return $this->revisions()->first(); + } } diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index bdd1e37b1..d6736886e 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -1,6 +1,7 @@ bookshelf = $bookshelf; $this->book = $book; $this->chapter = $chapter; $this->page = $page; $this->pageRevision = $pageRevision; $this->entities = [ + 'bookshelf' => $this->bookshelf, 'page' => $this->page, 'chapter' => $this->chapter, 'book' => $this->book @@ -331,6 +340,17 @@ class EntityRepo ->skip($count * $page)->take($count)->get(); } + /** + * Get the child items for a chapter sorted by priority but + * with draft items floated to the top. + * @param Bookshelf $bookshelf + * @return \Illuminate\Database\Eloquent\Collection|static[] + */ + public function getBookshelfChildren(Bookshelf $bookshelf) + { + return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get(); + } + /** * Get all child objects of a book. * Returns a sorted collection of Pages and Chapters. @@ -533,6 +553,28 @@ class EntityRepo return $entityModel; } + /** + * Sync the books assigned to a shelf from a comma-separated list + * of book IDs. + * @param Bookshelf $shelf + * @param string $books + */ + public function updateShelfBooks(Bookshelf $shelf, string $books) + { + $ids = explode(',', $books); + + // Check books exist and match ordering + $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id'); + $syncData = []; + foreach ($ids as $index => $id) { + if ($bookIds->contains($id)) { + $syncData[$id] = ['order' => $index]; + } + } + + $shelf->books()->sync($syncData); + } + /** * Change the book that an entity belongs to. * @param string $type @@ -704,10 +746,13 @@ class EntityRepo $revision->revision_number = $page->revision_count; $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(); + $revisionLimit = config('app.revision_limit'); + if ($revisionLimit !== false) { + $revisionsToDelete = $this->pageRevision->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']); + if ($revisionsToDelete->count() > 0) { + $this->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete(); + } } return $revision; @@ -1154,9 +1199,22 @@ class EntityRepo $this->permissionService->buildJointPermissionsForEntity($book); } + /** + * Destroy a bookshelf instance + * @param Bookshelf $shelf + * @throws \Throwable + */ + public function destroyBookshelf(Bookshelf $shelf) + { + $this->destroyEntityCommonRelations($shelf); + $shelf->delete(); + } + /** * Destroy the provided book and all its child entities. * @param Book $book + * @throws NotifyException + * @throws \Throwable */ public function destroyBook(Book $book) { @@ -1166,17 +1224,14 @@ class EntityRepo foreach ($book->chapters as $chapter) { $this->destroyChapter($chapter); } - \Activity::removeEntity($book); - $book->views()->delete(); - $book->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($book); - $this->searchService->deleteEntityTerms($book); + $this->destroyEntityCommonRelations($book); $book->delete(); } /** * Destroy a chapter and its relations. * @param Chapter $chapter + * @throws \Throwable */ public function destroyChapter(Chapter $chapter) { @@ -1186,11 +1241,7 @@ class EntityRepo $page->save(); } } - \Activity::removeEntity($chapter); - $chapter->views()->delete(); - $chapter->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($chapter); - $this->searchService->deleteEntityTerms($chapter); + $this->destroyEntityCommonRelations($chapter); $chapter->delete(); } @@ -1198,23 +1249,18 @@ class EntityRepo * Destroy a given page along with its dependencies. * @param Page $page * @throws NotifyException + * @throws \Throwable */ public function destroyPage(Page $page) { - \Activity::removeEntity($page); - $page->views()->delete(); - $page->tags()->delete(); - $page->revisions()->delete(); - $page->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($page); - $this->searchService->deleteEntityTerms($page); - // Check if set as custom homepage $customHome = setting('app-homepage', '0:'); if (intval($page->id) === intval(explode(':', $customHome)[0])) { throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl()); } + $this->destroyEntityCommonRelations($page); + // Delete Attached Files $attachmentService = app(AttachmentService::class); foreach ($page->attachments as $attachment) { @@ -1223,4 +1269,48 @@ class EntityRepo $page->delete(); } + + /** + * Destroy or handle the common relations connected to an entity. + * @param Entity $entity + * @throws \Throwable + */ + protected function destroyEntityCommonRelations(Entity $entity) + { + \Activity::removeEntity($entity); + $entity->views()->delete(); + $entity->permissions()->delete(); + $entity->tags()->delete(); + $entity->comments()->delete(); + $this->permissionService->deleteJointPermissionsForEntity($entity); + $this->searchService->deleteEntityTerms($entity); + } + + /** + * Copy the permissions of a bookshelf to all child books. + * Returns the number of books that had permissions updated. + * @param Bookshelf $bookshelf + * @return int + * @throws \Throwable + */ + public function copyBookshelfPermissions(Bookshelf $bookshelf) + { + $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray(); + $shelfBooks = $bookshelf->books()->get(); + $updatedBookCount = 0; + + foreach ($shelfBooks as $book) { + if (!userCan('restrictions-manage', $book)) { + continue; + } + $book->permissions()->delete(); + $book->restricted = $bookshelf->restricted; + $book->permissions()->createMany($shelfPermissions); + $book->save(); + $this->permissionService->buildJointPermissionsForEntity($book); + $updatedBookCount++; + } + + return $updatedBookCount; + } } diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index 6f7ea1dc8..68c9270be 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -80,7 +80,7 @@ class PermissionsRepo /** * Updates an existing role. - * Ensure Admin role always has all permissions. + * Ensure Admin role always have core permissions. * @param $roleId * @param $roleData * @throws PermissionsException @@ -90,13 +90,18 @@ class PermissionsRepo $role = $this->role->findOrFail($roleId); $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; - $this->assignRolePermissions($role, $permissions); - if ($role->system_name === 'admin') { - $permissions = $this->permission->all()->pluck('id')->toArray(); - $role->permissions()->sync($permissions); + $permissions = array_merge($permissions, [ + 'users-manage', + 'user-roles-manage', + 'restrictions-manage-all', + 'restrictions-manage-own', + 'settings-manage', + ]); } + $this->assignRolePermissions($role, $permissions); + $role->fill($roleData); $role->save(); $this->permissionService->buildJointPermissionForRole($role); diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index d113b676a..5143366fe 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -76,14 +76,15 @@ class UserRepo return $query->paginate($count); } - /** + /** * Creates a new user and attaches a role to them. * @param array $data + * @param boolean $verifyEmail * @return User */ - public function registerNew(array $data) + public function registerNew(array $data, $verifyEmail = false) { - $user = $this->create($data); + $user = $this->create($data, $verifyEmail); $this->attachDefaultRole($user); // Get avatar from gravatar and save @@ -94,15 +95,14 @@ class UserRepo /** * Give a user the default role. Used when creating a new user. - * @param $user + * @param User $user */ - public function attachDefaultRole($user) + public function attachDefaultRole(User $user) { $roleId = setting('registration-role'); - if ($roleId === false) { - $roleId = $this->role->first()->id; + if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) { + $user->attachRoleId($roleId); } - $user->attachRoleId($roleId); } /** @@ -141,15 +141,17 @@ class UserRepo /** * Create a new basic instance of user. * @param array $data + * @param boolean $verifyEmail * @return User */ - public function create(array $data) + public function create(array $data, $verifyEmail = false) { + return $this->user->forceCreate([ 'name' => $data['name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), - 'email_confirmed' => false + 'email_confirmed' => $verifyEmail ]); } diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index 73a677ac2..7b73c457c 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -316,25 +316,25 @@ class ImageService extends UploadService $deletedPaths = []; $this->image->newQuery()->whereIn('type', $types) - ->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) { - foreach ($images as $image) { - $searchQuery = '%' . basename($image->path) . '%'; - $inPage = DB::table('pages') + ->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) { + foreach ($images as $image) { + $searchQuery = '%' . basename($image->path) . '%'; + $inPage = DB::table('pages') ->where('html', 'like', $searchQuery)->count() > 0; - $inRevision = false; - if ($checkRevisions) { - $inRevision = DB::table('page_revisions') + $inRevision = false; + if ($checkRevisions) { + $inRevision = DB::table('page_revisions') ->where('html', 'like', $searchQuery)->count() > 0; - } + } - if (!$inPage && !$inRevision) { - $deletedPaths[] = $image->path; - if (!$dryRun) { - $this->destroy($image); - } - } - } - }); + if (!$inPage && !$inRevision) { + $deletedPaths[] = $image->path; + if (!$dryRun) { + $this->destroy($image); + } + } + } + }); return $deletedPaths; } diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php index 11223433b..16cee9f7d 100644 --- a/app/Services/LdapService.php +++ b/app/Services/LdapService.php @@ -330,14 +330,14 @@ class LdapService $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); } - $roles = Role::query()->where(function(Builder $query) use ($groupNames) { + $roles = Role::query()->where(function (Builder $query) use ($groupNames) { $query->whereIn('name', $groupNames); foreach ($groupNames as $groupName) { $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); } })->get(); - $matchedRoles = $roles->filter(function(Role $role) use ($groupNames) { + $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) { return $this->roleMatchesGroupNames($role, $groupNames); }); @@ -366,5 +366,4 @@ class LdapService $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); return in_array($roleName, $groupNames); } - } diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 0dd316b34..045824517 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -1,6 +1,7 @@ db = $db; $this->jointPermission = $jointPermission; $this->entityPermission = $entityPermission; $this->role = $role; + $this->bookshelf = $bookshelf; $this->book = $book; $this->chapter = $chapter; $this->page = $page; - // TODO - Update so admin still goes through filters } /** @@ -159,6 +170,12 @@ class PermissionService $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) { $this->buildJointPermissionsForBooks($books, $roles); }); + + // Chunk through all bookshelves + $this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); } /** @@ -174,6 +191,20 @@ class PermissionService }]); } + /** + * @param Collection $shelves + * @param array $roles + * @param bool $deleteOld + * @throws \Throwable + */ + protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false) + { + if ($deleteOld) { + $this->deleteManyJointPermissionsForEntities($shelves->all()); + } + $this->createManyJointPermissions($shelves, $roles); + } + /** * Build joint permissions for an array of books * @param Collection $books @@ -204,6 +235,7 @@ class PermissionService /** * Rebuild the entity jointPermissions for a particular entity. * @param Entity $entity + * @throws \Throwable */ public function buildJointPermissionsForEntity(Entity $entity) { @@ -214,7 +246,9 @@ class PermissionService return; } - $entities[] = $entity->book; + if ($entity->book) { + $entities[] = $entity->book; + } if ($entity->isA('page') && $entity->chapter_id) { $entities[] = $entity->chapter; @@ -226,13 +260,13 @@ class PermissionService } } - $this->deleteManyJointPermissionsForEntities($entities); $this->buildJointPermissionsForEntities(collect($entities)); } /** * Rebuild the entity jointPermissions for a collection of entities. * @param Collection $entities + * @throws \Throwable */ public function buildJointPermissionsForEntities(Collection $entities) { @@ -254,6 +288,12 @@ class PermissionService $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { $this->buildJointPermissionsForBooks($books, $roles); }); + + // Chunk through all bookshelves + $this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); } /** @@ -412,7 +452,7 @@ class PermissionService return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); } - if ($entity->isA('book')) { + if ($entity->isA('book') || $entity->isA('bookshelf')) { return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); } @@ -484,11 +524,6 @@ class PermissionService */ public function checkOwnableUserAccess(Ownable $ownable, $permission) { - if ($this->isAdmin()) { - $this->clean(); - return true; - } - $explodedPermission = explode('-', $permission); $baseQuery = $ownable->where('id', '=', $ownable->id); @@ -581,17 +616,16 @@ class PermissionService $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U")) ->mergeBindings($pageSelect)->mergeBindings($chapterSelect); - 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); - }); + // Add joint permission filter + $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); - } + }); + $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery); $query->orderBy('draft', 'desc')->orderBy('priority', 'asc'); $this->clean(); @@ -619,11 +653,6 @@ class PermissionService }); } - if ($this->isAdmin()) { - $this->clean(); - return $query; - } - $this->currentAction = $action; return $this->entityRestrictionQuery($query); } @@ -639,10 +668,6 @@ class PermissionService */ public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view') { - if ($this->isAdmin()) { - $this->clean(); - return $query; - } $this->currentAction = $action; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; @@ -675,11 +700,6 @@ class PermissionService */ public function filterRelatedPages($query, $tableName, $entityIdColumn) { - if ($this->isAdmin()) { - $this->clean(); - return $query; - } - $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; @@ -704,19 +724,6 @@ class PermissionService return $q; } - /** - * Check if the current user is an admin. - * @return bool - */ - private function isAdmin() - { - if ($this->isAdminUser === null) { - $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false; - } - - return $this->isAdminUser; - } - /** * Get the current user * @return User diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index 6390b8bc4..dfde645a1 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -1,6 +1,7 @@ searchTerm = $searchTerm; + $this->bookshelf = $bookshelf; $this->book = $book; $this->chapter = $chapter; $this->page = $page; $this->db = $db; $this->entities = [ + 'bookshelf' => $this->bookshelf, 'page' => $this->page, 'chapter' => $this->chapter, 'book' => $this->book @@ -65,6 +74,7 @@ class SearchService * @param string $entityType * @param int $page * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed. + * @param string $action * @return array[int, Collection]; */ public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view') @@ -370,20 +380,12 @@ class SearchService { $this->searchTerm->truncate(); - // Chunk through all books - $this->book->chunk(1000, function ($books) { - $this->indexEntities($books); - }); - - // Chunk through all chapters - $this->chapter->chunk(1000, function ($chapters) { - $this->indexEntities($chapters); - }); - - // Chunk through all pages - $this->page->chunk(1000, function ($pages) { - $this->indexEntities($pages); - }); + foreach ($this->entities as $entityModel) { + $selectFields = ['id', 'name', $entityModel->textField]; + $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) { + $this->indexEntities($entities); + }); + } } /** diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php index dac6b7773..9017dfebe 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Services/SocialAuthService.php @@ -1,13 +1,12 @@ validateDriver($socialDriver); - - // Get user details from social driver - $socialUser = $this->socialite->driver($driver)->user(); - // Check social account has not already been used if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) { throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login'); @@ -84,17 +78,26 @@ class SocialAuthService } /** - * Handle the login process on a oAuth callback. - * @param $socialDriver - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * Get the social user details via the social driver. + * @param string $socialDriver + * @return SocialUser * @throws SocialDriverNotConfigured - * @throws SocialSignInException */ - public function handleLoginCallback($socialDriver) + public function getSocialUser(string $socialDriver) { $driver = $this->validateDriver($socialDriver); - // Get user details from social driver - $socialUser = $this->socialite->driver($driver)->user(); + return $this->socialite->driver($driver)->user(); + } + + /** + * Handle the login process on a oAuth callback. + * @param $socialDriver + * @param SocialUser $socialUser + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws SocialSignInAccountNotUsed + */ + public function handleLoginCallback($socialDriver, SocialUser $socialUser) + { $socialId = $socialUser->getId(); // Get any attached social accounts or users @@ -136,7 +139,7 @@ class SocialAuthService $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]); } - throw new SocialSignInException($message, '/login'); + throw new SocialSignInAccountNotUsed($message, '/login'); } /** @@ -199,8 +202,28 @@ class SocialAuthService } /** - * @param string $socialDriver - * @param \Laravel\Socialite\Contracts\User $socialUser + * Check if the current config for the given driver allows auto-registration. + * @param string $driver + * @return bool + */ + public function driverAutoRegisterEnabled(string $driver) + { + return config('services.' . strtolower($driver) . '.auto_register') === true; + } + + /** + * Check if the current config for the given driver allow email address auto-confirmation. + * @param string $driver + * @return bool + */ + public function driverAutoConfirmEmailEnabled(string $driver) + { + return config('services.' . strtolower($driver) . '.auto_confirm') === true; + } + + /** + * @param string $socialDriver + * @param SocialUser $socialUser * @return SocialAccount */ public function fillSocialAccount($socialDriver, $socialUser) diff --git a/composer.json b/composer.json index c9eb3821c..3c372da63 100644 --- a/composer.json +++ b/composer.json @@ -6,9 +6,10 @@ "type": "project", "require": { "php": ">=7.0.0", + "ext-tidy": "*", + "ext-dom": "*", "laravel/framework": "~5.5.42", "fideloper/proxy": "~3.3", - "ext-tidy": "*", "intervention/image": "^2.4", "laravel/socialite": "^3.0", "league/flysystem-aws-s3-v3": "^1.0", @@ -21,7 +22,8 @@ "socialiteproviders/okta": "^1.0", "socialiteproviders/gitlab": "^3.0", "socialiteproviders/twitch": "^3.0", - "socialiteproviders/discord": "^2.0" + "socialiteproviders/discord": "^2.0", + "doctrine/dbal": "^2.5" }, "require-dev": { "filp/whoops": "~2.0", diff --git a/composer.lock b/composer.lock index 51fde846a..6a92409dd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "b98be6702f1293174f785f99895e798b", + "content-hash": "eae00f50d183bb224c934683b06e8f9c", "packages": [ { "name": "aws/aws-sdk-php", @@ -254,6 +254,355 @@ ], "time": "2014-05-19T10:25:02+00:00" }, + { + "name": "doctrine/annotations", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2017-02-24T16:22:25+00:00" + }, + { + "name": "doctrine/cache", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", + "shasum": "" + }, + "require": { + "php": "~5.5|~7.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.8|~5.0", + "predis/predis": "~1.0", + "satooshi/php-coveralls": "~0.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2017-07-22T12:49:21+00:00" + }, + { + "name": "doctrine/collections", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Collections\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "time": "2017-01-03T10:49:41+00:00" + }, + { + "name": "doctrine/common", + "version": "v2.7.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9", + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9", + "shasum": "" + }, + "require": { + "doctrine/annotations": "1.*", + "doctrine/cache": "1.*", + "doctrine/collections": "1.*", + "doctrine/inflector": "1.*", + "doctrine/lexer": "1.*", + "php": "~5.6|~7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common Library for Doctrine projects", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "collections", + "eventmanager", + "persistence", + "spl" + ], + "time": "2017-07-22T08:35:12+00:00" + }, + { + "name": "doctrine/dbal", + "version": "v2.5.13", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "729340d8d1eec8f01bff708e12e449a3415af873" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873", + "reference": "729340d8d1eec8f01bff708e12e449a3415af873", + "shasum": "" + }, + "require": { + "doctrine/common": ">=2.4,<2.8-dev", + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "4.*", + "symfony/console": "2.*||^3.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\DBAL\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Database Abstraction Layer", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "persistence", + "queryobject" + ], + "time": "2017-07-22T20:44:48+00:00" + }, { "name": "doctrine/inflector", "version": "v1.2.0", @@ -5464,7 +5813,8 @@ "prefer-lowest": false, "platform": { "php": ">=7.0.0", - "ext-tidy": "*" + "ext-tidy": "*", + "ext-dom": "*" }, "platform-dev": [], "platform-overrides": { diff --git a/config/app.php b/config/app.php index b0883b9be..9b5ac2f8c 100755 --- a/config/app.php +++ b/config/app.php @@ -12,6 +12,13 @@ return [ 'books' => env('APP_VIEWS_BOOKS', 'list') ], + /** + * The number of revisions to keep in the database. + * Once this limit is reached older revisions will be deleted. + * If set to false then a limit will not be enforced. + */ + 'revision_limit' => env('REVISION_LIMIT', 50), + /** * Allow