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..fb1c6d48b 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;
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..1167ea7da 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
@@ -1154,9 +1196,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 +1221,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 +1238,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 +1246,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 +1266,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..6fe8d2619 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
@@ -141,15 +142,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/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/config/app.php b/config/app.php
index b0883b9be..21ac6441b 100755
--- a/config/app.php
+++ b/config/app.php
@@ -77,8 +77,20 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
+ 'locales' => ['en', 'ar', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN', 'zh_TW'],
- 'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN', 'zh_TW'],
+ /*
+ |--------------------------------------------------------------------------
+ | Right-to-left text control
+ |--------------------------------------------------------------------------
+ |
+ | Right-to-left text control is set to false by default since English
+ | is the primary supported application but this may be dynamically
+ | altered by the applications localization system.
+ |
+ */
+
+ 'rtl' => false,
/*
|--------------------------------------------------------------------------
diff --git a/config/services.php b/config/services.php
index fab0c1d75..2b0f260cd 100644
--- a/config/services.php
+++ b/config/services.php
@@ -48,6 +48,8 @@ return [
'client_secret' => env('GITHUB_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/github/callback',
'name' => 'GitHub',
+ 'auto_register' => env('GITHUB_AUTO_REGISTER', false),
+ 'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
],
'google' => [
@@ -55,6 +57,8 @@ return [
'client_secret' => env('GOOGLE_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/google/callback',
'name' => 'Google',
+ 'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
+ 'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
],
'slack' => [
@@ -62,6 +66,8 @@ return [
'client_secret' => env('SLACK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/slack/callback',
'name' => 'Slack',
+ 'auto_register' => env('SLACK_AUTO_REGISTER', false),
+ 'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
],
'facebook' => [
@@ -69,6 +75,8 @@ return [
'client_secret' => env('FACEBOOK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
'name' => 'Facebook',
+ 'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
+ 'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
],
'twitter' => [
@@ -76,6 +84,8 @@ return [
'client_secret' => env('TWITTER_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
'name' => 'Twitter',
+ 'auto_register' => env('TWITTER_AUTO_REGISTER', false),
+ 'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
],
'azure' => [
@@ -84,6 +94,8 @@ return [
'tenant' => env('AZURE_TENANT', false),
'redirect' => env('APP_URL') . '/login/service/azure/callback',
'name' => 'Microsoft Azure',
+ 'auto_register' => env('AZURE_AUTO_REGISTER', false),
+ 'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
],
'okta' => [
@@ -92,6 +104,8 @@ return [
'redirect' => env('APP_URL') . '/login/service/okta/callback',
'base_url' => env('OKTA_BASE_URL'),
'name' => 'Okta',
+ 'auto_register' => env('OKTA_AUTO_REGISTER', false),
+ 'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
],
'gitlab' => [
@@ -100,6 +114,8 @@ return [
'redirect' => env('APP_URL') . '/login/service/gitlab/callback',
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
'name' => 'GitLab',
+ 'auto_register' => env('GITLAB_AUTO_REGISTER', false),
+ 'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
],
'twitch' => [
@@ -107,12 +123,17 @@ return [
'client_secret' => env('TWITCH_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
'name' => 'Twitch',
+ 'auto_register' => env('TWITCH_AUTO_REGISTER', false),
+ 'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
],
+
'discord' => [
'client_id' => env('DISCORD_APP_ID'),
'client_secret' => env('DISCORD_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/discord/callback',
'name' => 'Discord',
+ 'auto_register' => env('DISCORD_AUTO_REGISTER', false),
+ 'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
],
'ldap' => [
diff --git a/config/session.php b/config/session.php
index b334ffb3c..328fed27d 100644
--- a/config/session.php
+++ b/config/session.php
@@ -109,7 +109,7 @@ return [
|
*/
- 'cookie' => 'laravel_session',
+ 'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),
/*
|--------------------------------------------------------------------------
diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php
index c68f5c1e1..3d6ed1d63 100644
--- a/database/factories/ModelFactory.php
+++ b/database/factories/ModelFactory.php
@@ -21,6 +21,14 @@ $factory->define(BookStack\User::class, function ($faker) {
];
});
+$factory->define(BookStack\Bookshelf::class, function ($faker) {
+ return [
+ 'name' => $faker->sentence,
+ 'slug' => str_random(10),
+ 'description' => $faker->paragraph
+ ];
+});
+
$factory->define(BookStack\Book::class, function ($faker) {
return [
'name' => $faker->sentence,
diff --git a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php
index 4c1b43c4e..ce11f7b88 100644
--- a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php
+++ b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php
@@ -74,10 +74,6 @@ class CreateJointPermissionsTable extends Migration
// Update admin role with system name
DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
-
- // Generate the new entity jointPermissions
- $restrictionService = app(\BookStack\Services\PermissionService::class);
- $restrictionService->buildJointPermissions();
}
/**
diff --git a/database/migrations/2018_08_04_115700_create_bookshelves_table.php b/database/migrations/2018_08_04_115700_create_bookshelves_table.php
new file mode 100644
index 000000000..e92b0edef
--- /dev/null
+++ b/database/migrations/2018_08_04_115700_create_bookshelves_table.php
@@ -0,0 +1,101 @@
+increments('id');
+ $table->string('name', 200);
+ $table->string('slug', 200);
+ $table->text('description');
+ $table->integer('created_by')->nullable()->default(null);
+ $table->integer('updated_by')->nullable()->default(null);
+ $table->boolean('restricted')->default(false);
+ $table->integer('image_id')->nullable()->default(null);
+ $table->timestamps();
+
+ $table->index('slug');
+ $table->index('created_by');
+ $table->index('updated_by');
+ $table->index('restricted');
+ });
+
+ Schema::create('bookshelves_books', function (Blueprint $table) {
+ $table->integer('bookshelf_id')->unsigned();
+ $table->integer('book_id')->unsigned();
+ $table->integer('order')->unsigned();
+
+ $table->foreign('bookshelf_id')->references('id')->on('bookshelves')
+ ->onUpdate('cascade')->onDelete('cascade');
+ $table->foreign('book_id')->references('id')->on('books')
+ ->onUpdate('cascade')->onDelete('cascade');
+
+ $table->primary(['bookshelf_id', 'book_id']);
+ });
+
+ // Copy existing role permissions from Books
+ $ops = ['View All', 'View Own', 'Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
+ foreach ($ops as $op) {
+ $dbOpName = strtolower(str_replace(' ', '-', $op));
+ $roleIdsWithBookPermission = DB::table('role_permissions')
+ ->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')
+ ->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')
+ ->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');
+
+ $permId = DB::table('role_permissions')->insertGetId([
+ 'name' => 'bookshelf-' . $dbOpName,
+ 'display_name' => $op . ' ' . 'BookShelves',
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ ]);
+
+ $rowsToInsert = $roleIdsWithBookPermission->map(function($roleId) use ($permId) {
+ return [
+ 'role_id' => $roleId,
+ 'permission_id' => $permId
+ ];
+ })->toArray();
+
+ // Assign view permission to all current roles
+ DB::table('permission_role')->insert($rowsToInsert);
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ // Drop created permissions
+ $ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own'];
+
+ $permissionIds = DB::table('role_permissions')->whereIn('name', $ops)
+ ->get(['id'])->pluck('id')->toArray();
+ DB::table('permission_role')->whereIn('permission_id', $permissionIds)->delete();
+ DB::table('role_permissions')->whereIn('id', $permissionIds)->delete();
+
+ // Drop shelves table
+ Schema::dropIfExists('bookshelves_books');
+ Schema::dropIfExists('bookshelves');
+
+ // Drop related polymorphic items
+ DB::table('activities')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('views')->where('viewable_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('tags')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('search_terms')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ DB::table('comments')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
+ }
+}
diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php
index 41ac6650d..dcf589352 100644
--- a/database/seeds/DummyContentSeeder.php
+++ b/database/seeds/DummyContentSeeder.php
@@ -21,23 +21,29 @@ class DummyContentSeeder extends Seeder
$role = \BookStack\Role::getRole('viewer');
$viewerUser->attachRole($role);
- factory(\BookStack\Book::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
- ->each(function($book) use ($editorUser) {
- $chapters = factory(\BookStack\Chapter::class, 3)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
- ->each(function($chapter) use ($editorUser, $book){
- $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
+ $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
+
+ factory(\BookStack\Book::class, 5)->create($byData)
+ ->each(function($book) use ($editorUser, $byData) {
+ $chapters = factory(\BookStack\Chapter::class, 3)->create($byData)
+ ->each(function($chapter) use ($editorUser, $book, $byData){
+ $pages = factory(\BookStack\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
$chapter->pages()->saveMany($pages);
});
- $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $pages = factory(\BookStack\Page::class, 3)->make($byData);
$book->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages);
});
- $largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
- $pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
- $chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $largeBook = factory(\BookStack\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
+ $pages = factory(\BookStack\Page::class, 200)->make($byData);
+ $chapters = factory(\BookStack\Chapter::class, 50)->make($byData);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
+
+ $shelves = factory(\BookStack\Bookshelf::class, 10)->create($byData);
+ $largeBook->shelves()->attach($shelves->pluck('id'));
+
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
diff --git a/package-lock.json b/package-lock.json
index ec4da5ce2..f8c43993b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5857,6 +5857,21 @@
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
},
+ "jquery-sortable": {
+ "version": "0.9.13",
+ "resolved": "https://registry.npmjs.org/jquery-sortable/-/jquery-sortable-0.9.13.tgz",
+ "integrity": "sha1-HL+2VQE6B0c3BXHwbiL1JKAP+6I=",
+ "requires": {
+ "jquery": "^2.1.2"
+ },
+ "dependencies": {
+ "jquery": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz",
+ "integrity": "sha1-LInWiJterFIqfuoywUUhVZxsvwI="
+ }
+ }
+ },
"js-base64": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz",
diff --git a/package.json b/package.json
index 12d972cf9..58f2dad5e 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"codemirror": "^5.26.0",
"dropzone": "^5.4.0",
"jquery": "^3.3.1",
+ "jquery-sortable": "^0.9.13",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"vue": "^2.2.6",
diff --git a/phpunit.xml b/phpunit.xml
index 9434b710f..4c1e4f66c 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -35,8 +35,12 @@
{{ trans('entities.books_empty') }}
@if(userCan('books-create-all')) - @icon('edit'){{ trans('entities.create_one_now') }} + @icon('edit'){{ trans('entities.create_now') }} @endif @endif