Merge pull request #3 from BookStackApp/master

Getting the latest
This commit is contained in:
Abijeet Patro 2017-01-03 07:54:28 +05:30 committed by GitHub
commit cd6572b61a
159 changed files with 3272 additions and 2791 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ _ide_helper.php
/storage/debugbar /storage/debugbar
.phpstorm.meta.php .phpstorm.meta.php
yarn.lock yarn.lock
/bin

View File

@ -5,6 +5,8 @@ class Chapter extends Entity
{ {
protected $fillable = ['name', 'description', 'priority', 'book_id']; protected $fillable = ['name', 'description', 'priority', 'book_id'];
protected $with = ['book'];
/** /**
* Get the book this chapter is within. * Get the book this chapter is within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
@ -16,11 +18,12 @@ class Chapter extends Entity
/** /**
* Get the pages that this chapter contains. * Get the pages that this chapter contains.
* @param string $dir
* @return mixed * @return mixed
*/ */
public function pages() public function pages($dir = 'ASC')
{ {
return $this->hasMany(Page::class)->orderBy('priority', 'ASC'); return $this->hasMany(Page::class)->orderBy('priority', $dir);
} }
/** /**

View File

@ -4,6 +4,8 @@
class Entity extends Ownable class Entity extends Ownable
{ {
protected $fieldsToSearch = ['name', 'description'];
/** /**
* Compares this entity to another given entity. * Compares this entity to another given entity.
* Matches by comparing class and id. * Matches by comparing class and id.
@ -157,7 +159,7 @@ class Entity extends Ownable
* @param string[] array $wheres * @param string[] array $wheres
* @return mixed * @return mixed
*/ */
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = []) public function fullTextSearchQuery($terms, $wheres = [])
{ {
$exactTerms = []; $exactTerms = [];
$fuzzyTerms = []; $fuzzyTerms = [];
@ -181,16 +183,16 @@ class Entity extends Ownable
// Perform fulltext search if relevant terms exist. // Perform fulltext search if relevant terms exist.
if ($isFuzzy) { if ($isFuzzy) {
$termString = implode(' ', $fuzzyTerms); $termString = implode(' ', $fuzzyTerms);
$fields = implode(',', $fieldsToSearch); $fields = implode(',', $this->fieldsToSearch);
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]); $search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
} }
// Ensure at least one exact term matches if in search // Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) { if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms, $fieldsToSearch) { $search = $search->where(function ($query) use ($exactTerms) {
foreach ($exactTerms as $exactTerm) { foreach ($exactTerms as $exactTerm) {
foreach ($fieldsToSearch as $field) { foreach ($this->fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm); $query->orWhere($field, 'like', $exactTerm);
} }
} }

View File

@ -2,7 +2,7 @@
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment; use BookStack\Attachment;
use BookStack\Repos\PageRepo; use BookStack\Repos\EntityRepo;
use BookStack\Services\AttachmentService; use BookStack\Services\AttachmentService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -10,19 +10,19 @@ class AttachmentController extends Controller
{ {
protected $attachmentService; protected $attachmentService;
protected $attachment; protected $attachment;
protected $pageRepo; protected $entityRepo;
/** /**
* AttachmentController constructor. * AttachmentController constructor.
* @param AttachmentService $attachmentService * @param AttachmentService $attachmentService
* @param Attachment $attachment * @param Attachment $attachment
* @param PageRepo $pageRepo * @param EntityRepo $entityRepo
*/ */
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo) public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
{ {
$this->attachmentService = $attachmentService; $this->attachmentService = $attachmentService;
$this->attachment = $attachment; $this->attachment = $attachment;
$this->pageRepo = $pageRepo; $this->entityRepo = $entityRepo;
parent::__construct(); parent::__construct();
} }
@ -40,7 +40,7 @@ class AttachmentController extends Controller
]); ]);
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId, true); $page = $this->entityRepo->getById('page', $pageId, true);
$this->checkPermission('attachment-create-all'); $this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
@ -70,14 +70,14 @@ class AttachmentController extends Controller
]); ]);
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId, true); $page = $this->entityRepo->getById('page', $pageId, true);
$attachment = $this->attachment->findOrFail($attachmentId); $attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-create', $attachment); $this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) { if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError('Page mismatch during attached file update'); return $this->jsonError(trans('errors.attachment_page_mismatch'));
} }
$uploadedFile = $request->file('file'); $uploadedFile = $request->file('file');
@ -106,18 +106,18 @@ class AttachmentController extends Controller
]); ]);
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId, true); $page = $this->entityRepo->getById('page', $pageId, true);
$attachment = $this->attachment->findOrFail($attachmentId); $attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-create', $attachment); $this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) { if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError('Page mismatch during attachment update'); return $this->jsonError(trans('errors.attachment_page_mismatch'));
} }
$attachment = $this->attachmentService->updateFile($attachment, $request->all()); $attachment = $this->attachmentService->updateFile($attachment, $request->all());
return $attachment; return response()->json($attachment);
} }
/** /**
@ -134,7 +134,7 @@ class AttachmentController extends Controller
]); ]);
$pageId = $request->get('uploaded_to'); $pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId, true); $page = $this->entityRepo->getById('page', $pageId, true);
$this->checkPermission('attachment-create-all'); $this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
@ -153,7 +153,7 @@ class AttachmentController extends Controller
*/ */
public function listForPage($pageId) public function listForPage($pageId)
{ {
$page = $this->pageRepo->getById($pageId, true); $page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments); return response()->json($page->attachments);
} }
@ -170,12 +170,12 @@ class AttachmentController extends Controller
'files' => 'required|array', 'files' => 'required|array',
'files.*.id' => 'required|integer', 'files.*.id' => 'required|integer',
]); ]);
$page = $this->pageRepo->getById($pageId); $page = $this->entityRepo->getById('page', $pageId);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files'); $attachments = $request->get('files');
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId); $this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
return response()->json(['message' => 'Attachment order updated']); return response()->json(['message' => trans('entities.attachments_order_updated')]);
} }
/** /**
@ -186,7 +186,7 @@ class AttachmentController extends Controller
public function get($attachmentId) public function get($attachmentId)
{ {
$attachment = $this->attachment->findOrFail($attachmentId); $attachment = $this->attachment->findOrFail($attachmentId);
$page = $this->pageRepo->getById($attachment->uploaded_to); $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
if ($attachment->external) { if ($attachment->external) {
@ -210,6 +210,6 @@ class AttachmentController extends Controller
$attachment = $this->attachment->findOrFail($attachmentId); $attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment); $this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment); $this->attachmentService->deleteFile($attachment);
return response()->json(['message' => 'Attachment deleted']); return response()->json(['message' => trans('entities.attachments_deleted')]);
} }
} }

View File

@ -52,7 +52,7 @@ class ForgotPasswordController extends Controller
); );
if ($response === Password::RESET_LINK_SENT) { if ($response === Password::RESET_LINK_SENT) {
$message = 'A password reset link has been sent to ' . $request->get('email') . '.'; $message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
session()->flash('success', $message); session()->flash('success', $message);
return back()->with('status', trans($response)); return back()->with('status', trans($response));
} }

View File

@ -87,7 +87,7 @@ class LoginController extends Controller
// Check for users with same email already // Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0; $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) { if ($alreadyUser) {
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.'); throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
} }
$user->save(); $user->save();

View File

@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers\Auth; namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\ConfirmationEmailException; use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use BookStack\Services\EmailConfirmationService; use BookStack\Services\EmailConfirmationService;
@ -82,7 +83,7 @@ class RegisterController extends Controller
protected function checkRegistrationAllowed() protected function checkRegistrationAllowed()
{ {
if (!setting('registration-enabled')) { if (!setting('registration-enabled')) {
throw new UserRegistrationException('Registrations are currently disabled.', '/login'); throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
} }
} }
@ -147,7 +148,7 @@ class RegisterController extends Controller
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict'))); $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1); $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) { if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException('That email domain does not have access to this application', '/register'); throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
} }
} }
@ -169,7 +170,7 @@ class RegisterController extends Controller
} }
auth()->login($newUser); auth()->login($newUser);
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.'); session()->flash('success', trans('auth.register_success'));
return redirect($this->redirectPath()); return redirect($this->redirectPath());
} }
@ -262,7 +263,7 @@ class RegisterController extends Controller
return $this->socialRegisterCallback($socialDriver); return $this->socialRegisterCallback($socialDriver);
} }
} else { } else {
throw new SocialSignInException('No action defined', '/login'); throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
} }
return redirect()->back(); return redirect()->back();
} }

View File

@ -41,7 +41,7 @@ class ResetPasswordController extends Controller
*/ */
protected function sendResetResponse($response) protected function sendResetResponse($response)
{ {
$message = 'Your password has been successfully reset.'; $message = trans('auth.reset_password_success');
session()->flash('success', $message); session()->flash('success', $message);
return redirect($this->redirectPath()) return redirect($this->redirectPath())
->with('status', trans($response)); ->with('status', trans($response));

View File

@ -1,34 +1,26 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests; use Illuminate\Http\Response;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Views; use Views;
class BookController extends Controller class BookController extends Controller
{ {
protected $bookRepo; protected $entityRepo;
protected $pageRepo;
protected $chapterRepo;
protected $userRepo; protected $userRepo;
/** /**
* BookController constructor. * BookController constructor.
* @param BookRepo $bookRepo * @param EntityRepo $entityRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo * @param UserRepo $userRepo
*/ */
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo) public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
{ {
$this->bookRepo = $bookRepo; $this->entityRepo = $entityRepo;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
} }
@ -39,9 +31,9 @@ class BookController extends Controller
*/ */
public function index() public function index()
{ {
$books = $this->bookRepo->getAllPaginated(10); $books = $this->entityRepo->getAllPaginated('book', 10);
$recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false; $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->bookRepo->getPopular(4, 0); $popular = $this->entityRepo->getPopular('book', 4, 0);
$this->setPageTitle('Books'); $this->setPageTitle('Books');
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]); return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
} }
@ -53,7 +45,7 @@ class BookController extends Controller
public function create() public function create()
{ {
$this->checkPermission('book-create-all'); $this->checkPermission('book-create-all');
$this->setPageTitle('Create New Book'); $this->setPageTitle(trans('entities.books_create'));
return view('books/create'); return view('books/create');
} }
@ -70,7 +62,7 @@ class BookController extends Controller
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000' 'description' => 'string|max:1000'
]); ]);
$book = $this->bookRepo->createFromInput($request->all()); $book = $this->entityRepo->createFromInput('book', $request->all());
Activity::add($book, 'book_create', $book->id); Activity::add($book, 'book_create', $book->id);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -82,9 +74,9 @@ class BookController extends Controller
*/ */
public function show($slug) public function show($slug)
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book); $this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->bookRepo->getChildren($book); $bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book); Views::add($book);
$this->setPageTitle($book->getShortName()); $this->setPageTitle($book->getShortName());
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]); return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
@ -97,9 +89,9 @@ class BookController extends Controller
*/ */
public function edit($slug) public function edit($slug)
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$this->setPageTitle('Edit Book ' . $book->getShortName()); $this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]); return view('books/edit', ['book' => $book, 'current' => $book]);
} }
@ -111,13 +103,13 @@ class BookController extends Controller
*/ */
public function update(Request $request, $slug) public function update(Request $request, $slug)
{ {
$book = $this->bookRepo->getBySlug($slug); $book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'string|max:1000' 'description' => 'string|max:1000'
]); ]);
$book = $this->bookRepo->updateFromInput($book, $request->all()); $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id); Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -129,9 +121,9 @@ class BookController extends Controller
*/ */
public function showDelete($bookSlug) public function showDelete($bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle('Delete Book ' . $book->getShortName()); $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
return view('books/delete', ['book' => $book, 'current' => $book]); return view('books/delete', ['book' => $book, 'current' => $book]);
} }
@ -142,11 +134,11 @@ class BookController extends Controller
*/ */
public function sort($bookSlug) public function sort($bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->bookRepo->getChildren($book, true); $bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->bookRepo->getAll(false); $books = $this->entityRepo->getAll('book', false);
$this->setPageTitle('Sort Book ' . $book->getShortName()); $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
} }
@ -158,8 +150,8 @@ class BookController extends Controller
*/ */
public function getSortItem($bookSlug) public function getSortItem($bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$bookChildren = $this->bookRepo->getChildren($book); $bookChildren = $this->entityRepo->getBookChildren($book);
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
} }
@ -171,7 +163,7 @@ class BookController extends Controller
*/ */
public function saveSort($bookSlug, Request $request) public function saveSort($bookSlug, Request $request)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
// Return if no map sent // Return if no map sent
@ -190,13 +182,13 @@ class BookController extends Controller
$priority = $bookChild->sort; $priority = $bookChild->sort;
$id = intval($bookChild->id); $id = intval($bookChild->id);
$isPage = $bookChild->type == 'page'; $isPage = $bookChild->type == 'page';
$bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId; $bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter); $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id); $model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
// Update models only if there's a change in parent chain or ordering. // Update models only if there's a change in parent chain or ordering.
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) { if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model); $this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
$model->priority = $priority; $model->priority = $priority;
if ($isPage) $model->chapter_id = $chapterId; if ($isPage) $model->chapter_id = $chapterId;
$model->save(); $model->save();
@ -211,12 +203,12 @@ class BookController extends Controller
// Add activity for books // Add activity for books
foreach ($sortedBooks as $bookId) { foreach ($sortedBooks as $bookId) {
$updatedBook = $this->bookRepo->getById($bookId); $updatedBook = $this->entityRepo->getById('book', $bookId);
Activity::add($updatedBook, 'book_sort', $updatedBook->id); Activity::add($updatedBook, 'book_sort', $updatedBook->id);
} }
// Update permissions on changed models // Update permissions on changed models
$this->bookRepo->buildJointPermissions($updatedModels); $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -228,11 +220,10 @@ class BookController extends Controller
*/ */
public function destroy($bookSlug) public function destroy($bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book); $this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name); Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book); $this->entityRepo->destroyBook($book);
$this->bookRepo->destroy($book);
return redirect('/books'); return redirect('/books');
} }
@ -243,7 +234,7 @@ class BookController extends Controller
*/ */
public function showRestrict($bookSlug) public function showRestrict($bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book); $this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles(); $roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [ return view('books/restrictions', [
@ -261,10 +252,10 @@ class BookController extends Controller
*/ */
public function restrict($bookSlug, Request $request) public function restrict($bookSlug, Request $request)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book); $this->checkOwnablePermission('restrictions-manage', $book);
$this->bookRepo->updateEntityPermissionsFromRequest($request, $book); $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
session()->flash('success', 'Book Restrictions Updated'); session()->flash('success', trans('entities.books_permissions_updated'));
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
} }

View File

@ -1,30 +1,26 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests; use Illuminate\Http\Response;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use Views; use Views;
class ChapterController extends Controller class ChapterController extends Controller
{ {
protected $bookRepo;
protected $chapterRepo;
protected $userRepo; protected $userRepo;
protected $entityRepo;
/** /**
* ChapterController constructor. * ChapterController constructor.
* @param BookRepo $bookRepo * @param EntityRepo $entityRepo
* @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo * @param UserRepo $userRepo
*/ */
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo) public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
{ {
$this->bookRepo = $bookRepo; $this->entityRepo = $entityRepo;
$this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
} }
@ -36,9 +32,9 @@ class ChapterController extends Controller
*/ */
public function create($bookSlug) public function create($bookSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book); $this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle('Create New Chapter'); $this->setPageTitle(trans('entities.chapters_create'));
return view('chapters/create', ['book' => $book, 'current' => $book]); return view('chapters/create', ['book' => $book, 'current' => $book]);
} }
@ -54,12 +50,12 @@ class ChapterController extends Controller
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book); $this->checkOwnablePermission('chapter-create', $book);
$input = $request->all(); $input = $request->all();
$input['priority'] = $this->bookRepo->getNewPriority($book); $input['priority'] = $this->entityRepo->getNewBookPriority($book);
$chapter = $this->chapterRepo->createFromInput($input, $book); $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
Activity::add($chapter, 'chapter_create', $book->id); Activity::add($chapter, 'chapter_create', $book->id);
return redirect($chapter->getUrl()); return redirect($chapter->getUrl());
} }
@ -72,15 +68,14 @@ class ChapterController extends Controller
*/ */
public function show($bookSlug, $chapterSlug) public function show($bookSlug, $chapterSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-view', $chapter); $this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = $this->bookRepo->getChildren($book); $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
Views::add($chapter); Views::add($chapter);
$this->setPageTitle($chapter->getShortName()); $this->setPageTitle($chapter->getShortName());
$pages = $this->chapterRepo->getChildren($chapter); $pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters/show', [ return view('chapters/show', [
'book' => $book, 'book' => $chapter->book,
'chapter' => $chapter, 'chapter' => $chapter,
'current' => $chapter, 'current' => $chapter,
'sidebarTree' => $sidebarTree, 'sidebarTree' => $sidebarTree,
@ -96,11 +91,10 @@ class ChapterController extends Controller
*/ */
public function edit($bookSlug, $chapterSlug) public function edit($bookSlug, $chapterSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle('Edit Chapter' . $chapter->getShortName()); $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
} }
/** /**
@ -112,16 +106,15 @@ class ChapterController extends Controller
*/ */
public function update(Request $request, $bookSlug, $chapterSlug) public function update(Request $request, $bookSlug, $chapterSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
if ($chapter->name !== $request->get('name')) { if ($chapter->name !== $request->get('name')) {
$chapter->slug = $this->chapterRepo->findSuitableSlug($request->get('name'), $book->id, $chapter->id); $chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
} }
$chapter->fill($request->all()); $chapter->fill($request->all());
$chapter->updated_by = user()->id; $chapter->updated_by = user()->id;
$chapter->save(); $chapter->save();
Activity::add($chapter, 'chapter_update', $book->id); Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl()); return redirect($chapter->getUrl());
} }
@ -133,11 +126,10 @@ class ChapterController extends Controller
*/ */
public function showDelete($bookSlug, $chapterSlug) public function showDelete($bookSlug, $chapterSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle('Delete Chapter' . $chapter->getShortName()); $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
} }
/** /**
@ -148,11 +140,11 @@ class ChapterController extends Controller
*/ */
public function destroy($bookSlug, $chapterSlug) public function destroy($bookSlug, $chapterSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $book = $chapter->book;
$this->checkOwnablePermission('chapter-delete', $chapter); $this->checkOwnablePermission('chapter-delete', $chapter);
Activity::addMessage('chapter_delete', $book->id, $chapter->name); Activity::addMessage('chapter_delete', $book->id, $chapter->name);
$this->chapterRepo->destroy($chapter); $this->entityRepo->destroyChapter($chapter);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -164,12 +156,12 @@ class ChapterController extends Controller
* @throws \BookStack\Exceptions\NotFoundException * @throws \BookStack\Exceptions\NotFoundException
*/ */
public function showMove($bookSlug, $chapterSlug) { public function showMove($bookSlug, $chapterSlug) {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
return view('chapters/move', [ return view('chapters/move', [
'chapter' => $chapter, 'chapter' => $chapter,
'book' => $book 'book' => $chapter->book
]); ]);
} }
@ -182,8 +174,7 @@ class ChapterController extends Controller
* @throws \BookStack\Exceptions\NotFoundException * @throws \BookStack\Exceptions\NotFoundException
*/ */
public function move($bookSlug, $chapterSlug, Request $request) { public function move($bookSlug, $chapterSlug, Request $request) {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter); $this->checkOwnablePermission('chapter-update', $chapter);
$entitySelection = $request->get('entity_selection', null); $entitySelection = $request->get('entity_selection', null);
@ -198,17 +189,17 @@ class ChapterController extends Controller
$parent = false; $parent = false;
if ($entityType == 'book') { if ($entityType == 'book') {
$parent = $this->bookRepo->getById($entityId); $parent = $this->entityRepo->getById('book', $entityId);
} }
if ($parent === false || $parent === null) { if ($parent === false || $parent === null) {
session()->flash('The selected Book was not found'); session()->flash('error', trans('errors.selected_book_not_found'));
return redirect()->back(); return redirect()->back();
} }
$this->chapterRepo->changeBook($parent->id, $chapter, true); $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
Activity::add($chapter, 'chapter_move', $chapter->book->id); Activity::add($chapter, 'chapter_move', $chapter->book->id);
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name)); session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
return redirect($chapter->getUrl()); return redirect($chapter->getUrl());
} }
@ -221,8 +212,7 @@ class ChapterController extends Controller
*/ */
public function showRestrict($bookSlug, $chapterSlug) public function showRestrict($bookSlug, $chapterSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter); $this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles(); $roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [ return view('chapters/restrictions', [
@ -240,11 +230,10 @@ class ChapterController extends Controller
*/ */
public function restrict($bookSlug, $chapterSlug, Request $request) public function restrict($bookSlug, $chapterSlug, Request $request)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter); $this->checkOwnablePermission('restrictions-manage', $chapter);
$this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter); $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
session()->flash('success', 'Chapter Restrictions Updated'); session()->flash('success', trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl()); return redirect($chapter->getUrl());
} }
} }

View File

@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Repos\EntityRepo; use BookStack\Repos\EntityRepo;
use BookStack\Http\Requests; use BookStack\Http\Requests;
use Illuminate\Http\Response;
use Views; use Views;
class HomeController extends Controller class HomeController extends Controller
@ -31,9 +32,9 @@ class HomeController extends Controller
$activity = Activity::latest(10); $activity = Activity::latest(10);
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : []; $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
$recentFactor = count($draftPages) > 0 ? 0.5 : 1; $recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreatedBooks(10*$recentFactor); $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5); $recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
return view('home', [ return view('home', [
'activity' => $activity, 'activity' => $activity,
'recents' => $recents, 'recents' => $recents,
@ -43,4 +44,39 @@ class HomeController extends Controller
]); ]);
} }
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getTranslations() {
$locale = trans()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey);
} else {
$translations = [
// Get only translations which might be used in JS
'common' => trans('common'),
'components' => trans('components'),
'entities' => trans('entities'),
'errors' => trans('errors')
];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], null, 'en'),
'components' => trans('components', [], null, 'en'),
'entities' => trans('entities', [], null, 'en'),
'errors' => trans('errors', [], null, 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}
$resp = 'window.translations = ' . json_encode($translations);
cache()->put($cacheKey, $resp, 120);
}
return response($resp, 200, [
'Content-Type' => 'application/javascript'
]);
}
} }

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\ImageRepo; use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File; use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -73,6 +74,7 @@ class ImageController extends Controller
* @param $filter * @param $filter
* @param int $page * @param int $page
* @param Request $request * @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/ */
public function getGalleryFiltered($filter, $page = 0, Request $request) public function getGalleryFiltered($filter, $page = 0, Request $request)
{ {
@ -149,12 +151,12 @@ class ImageController extends Controller
/** /**
* Deletes an image and all thumbnail/image files * Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo * @param EntityRepo $entityRepo
* @param Request $request * @param Request $request
* @param int $id * @param int $id
* @return \Illuminate\Http\JsonResponse * @return \Illuminate\Http\JsonResponse
*/ */
public function destroy(PageRepo $pageRepo, Request $request, $id) public function destroy(EntityRepo $entityRepo, Request $request, $id)
{ {
$image = $this->imageRepo->getById($id); $image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image); $this->checkOwnablePermission('image-delete', $image);
@ -162,14 +164,14 @@ class ImageController extends Controller
// Check if this image is used on any pages // Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
if (!$isForced) { if (!$isForced) {
$pageSearch = $pageRepo->searchForImage($image->url); $pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) { if ($pageSearch !== false) {
return response()->json($pageSearch, 400); return response()->json($pageSearch, 400);
} }
} }
$this->imageRepo->destroyImage($image); $this->imageRepo->destroyImage($image);
return response()->json('Image Deleted'); return response()->json(trans('components.images_deleted'));
} }

View File

@ -2,40 +2,31 @@
use Activity; use Activity;
use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService; use BookStack\Services\ExportService;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests; use Illuminate\Http\Response;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Views; use Views;
use GatherContent\Htmldiff\Htmldiff; use GatherContent\Htmldiff\Htmldiff;
class PageController extends Controller class PageController extends Controller
{ {
protected $pageRepo; protected $entityRepo;
protected $bookRepo;
protected $chapterRepo;
protected $exportService; protected $exportService;
protected $userRepo; protected $userRepo;
/** /**
* PageController constructor. * PageController constructor.
* @param PageRepo $pageRepo * @param EntityRepo $entityRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param ExportService $exportService * @param ExportService $exportService
* @param UserRepo $userRepo * @param UserRepo $userRepo
*/ */
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo) public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
{ {
$this->pageRepo = $pageRepo; $this->entityRepo = $entityRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService; $this->exportService = $exportService;
$this->userRepo = $userRepo; $this->userRepo = $userRepo;
parent::__construct(); parent::__construct();
@ -50,19 +41,19 @@ class PageController extends Controller
*/ */
public function create($bookSlug, $chapterSlug = null) public function create($bookSlug, $chapterSlug = null)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
$parent = $chapter ? $chapter : $book; $parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in // Redirect to draft edit screen if signed in
if ($this->signedIn) { if ($this->signedIn) {
$draft = $this->pageRepo->getDraftPage($book, $chapter); $draft = $this->entityRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl()); return redirect($draft->getUrl());
} }
// Otherwise show edit view // Otherwise show edit view
$this->setPageTitle('Create New Page'); $this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]); return view('pages/guest-create', ['parent' => $parent]);
} }
@ -80,13 +71,13 @@ class PageController extends Controller
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
$parent = $chapter ? $chapter : $book; $parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
$page = $this->pageRepo->getDraftPage($book, $chapter); $page = $this->entityRepo->getDraftPage($book, $chapter);
$this->pageRepo->publishDraft($page, [ $this->entityRepo->publishPageDraft($page, [
'name' => $request->get('name'), 'name' => $request->get('name'),
'html' => '' 'html' => ''
]); ]);
@ -101,15 +92,14 @@ class PageController extends Controller
*/ */
public function editDraft($bookSlug, $pageId) public function editDraft($bookSlug, $pageId)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $draft = $this->entityRepo->getById('page', $pageId, true);
$draft = $this->pageRepo->getById($pageId, true); $this->checkOwnablePermission('page-create', $draft->book);
$this->checkOwnablePermission('page-create', $book); $this->setPageTitle(trans('entities.pages_edit_draft'));
$this->setPageTitle('Edit Page Draft');
$draftsEnabled = $this->signedIn; $draftsEnabled = $this->signedIn;
return view('pages/edit', [ return view('pages/edit', [
'page' => $draft, 'page' => $draft,
'book' => $book, 'book' => $draft->book,
'isDraft' => true, 'isDraft' => true,
'draftsEnabled' => $draftsEnabled 'draftsEnabled' => $draftsEnabled
]); ]);
@ -119,6 +109,7 @@ class PageController extends Controller
* Store a new page by changing a draft into a page. * Store a new page by changing a draft into a page.
* @param Request $request * @param Request $request
* @param string $bookSlug * @param string $bookSlug
* @param int $pageId
* @return Response * @return Response
*/ */
public function store(Request $request, $bookSlug, $pageId) public function store(Request $request, $bookSlug, $pageId)
@ -128,21 +119,21 @@ class PageController extends Controller
]); ]);
$input = $request->all(); $input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug); $book = $this->entityRepo->getBySlug('book', $bookSlug);
$draftPage = $this->pageRepo->getById($pageId, true); $draftPage = $this->entityRepo->getById('page', $pageId, true);
$chapterId = intval($draftPage->chapter_id); $chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book; $parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$this->checkOwnablePermission('page-create', $parent); $this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) { if ($parent->isA('chapter')) {
$input['priority'] = $this->chapterRepo->getNewPriority($parent); $input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
} else { } else {
$input['priority'] = $this->bookRepo->getNewPriority($parent); $input['priority'] = $this->entityRepo->getNewBookPriority($parent);
} }
$page = $this->pageRepo->publishDraft($draftPage, $input); $page = $this->entityRepo->publishPageDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id); Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
@ -150,32 +141,29 @@ class PageController extends Controller
/** /**
* Display the specified page. * Display the specified page.
* If the page is not found via the slug the * If the page is not found via the slug the revisions are searched for a match.
* revisions are searched for a match.
* @param string $bookSlug * @param string $bookSlug
* @param string $pageSlug * @param string $pageSlug
* @return Response * @return Response
*/ */
public function show($bookSlug, $pageSlug) public function show($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug);
try { try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
} catch (NotFoundException $e) { } catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug); $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404); if ($page === null) abort(404);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
$this->checkOwnablePermission('page-view', $page); $this->checkOwnablePermission('page-view', $page);
$sidebarTree = $this->bookRepo->getChildren($book); $sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->pageRepo->getPageNav($page); $pageNav = $this->entityRepo->getPageNav($page);
Views::add($page); Views::add($page);
$this->setPageTitle($page->getShortName()); $this->setPageTitle($page->getShortName());
return view('pages/show', ['page' => $page, 'book' => $book, return view('pages/show', ['page' => $page, 'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]); 'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]);
} }
@ -186,7 +174,7 @@ class PageController extends Controller
*/ */
public function getPageAjax($pageId) public function getPageAjax($pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->entityRepo->getById('page', $pageId);
return response()->json($page); return response()->json($page);
} }
@ -198,26 +186,25 @@ class PageController extends Controller
*/ */
public function edit($bookSlug, $pageSlug) public function edit($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Editing Page ' . $page->getShortName()); $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
$page->isDraft = false; $page->isDraft = false;
// Check for active editing // Check for active editing
$warnings = []; $warnings = [];
if ($this->pageRepo->isPageEditingActive($page, 60)) { if ($this->entityRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60); $warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
} }
// Check for a current draft version for this user // Check for a current draft version for this user
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); $draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
$page->name = $draft->name; $page->name = $draft->name;
$page->html = $draft->html; $page->html = $draft->html;
$page->markdown = $draft->markdown; $page->markdown = $draft->markdown;
$page->isDraft = true; $page->isDraft = true;
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft); $warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
} }
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings)); if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
@ -225,7 +212,7 @@ class PageController extends Controller
$draftsEnabled = $this->signedIn; $draftsEnabled = $this->signedIn;
return view('pages/edit', [ return view('pages/edit', [
'page' => $page, 'page' => $page,
'book' => $book, 'book' => $page->book,
'current' => $page, 'current' => $page,
'draftsEnabled' => $draftsEnabled 'draftsEnabled' => $draftsEnabled
]); ]);
@ -243,11 +230,10 @@ class PageController extends Controller
$this->validate($request, [ $this->validate($request, [
'name' => 'required|string|max:255' 'name' => 'required|string|max:255'
]); ]);
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->pageRepo->updatePage($page, $book->id, $request->all()); $this->entityRepo->updatePage($page, $page->book->id, $request->all());
Activity::add($page, 'page_update', $book->id); Activity::add($page, 'page_update', $page->book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -259,27 +245,23 @@ class PageController extends Controller
*/ */
public function saveDraft(Request $request, $pageId) public function saveDraft(Request $request, $pageId)
{ {
$page = $this->pageRepo->getById($pageId, true); $page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
if (!$this->signedIn) { if (!$this->signedIn) {
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
'message' => 'Guests cannot save drafts', 'message' => trans('errors.guests_cannot_save_drafts'),
], 500); ], 500);
} }
if ($page->draft) { $draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
} else {
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
}
$updateTime = $draft->updated_at->timestamp; $updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset; $utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([ return response()->json([
'status' => 'success', 'status' => 'success',
'message' => 'Draft saved at ', 'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $utcUpdateTimestamp 'timestamp' => $utcUpdateTimestamp
]); ]);
} }
@ -292,7 +274,7 @@ class PageController extends Controller
*/ */
public function redirectFromLink($pageId) public function redirectFromLink($pageId)
{ {
$page = $this->pageRepo->getById($pageId); $page = $this->entityRepo->getById('page', $pageId);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -304,11 +286,10 @@ class PageController extends Controller
*/ */
public function showDelete($bookSlug, $pageSlug) public function showDelete($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle('Delete Page ' . $page->getShortName()); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
} }
@ -321,11 +302,10 @@ class PageController extends Controller
*/ */
public function showDeleteDraft($bookSlug, $pageId) public function showDeleteDraft($bookSlug, $pageId)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getById('page', $pageId, true);
$page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Delete Draft Page ' . $page->getShortName()); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
} }
/** /**
@ -337,12 +317,12 @@ class PageController extends Controller
*/ */
public function destroy($bookSlug, $pageSlug) public function destroy($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $book = $page->book;
$this->checkOwnablePermission('page-delete', $page); $this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name); Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', 'Page deleted'); session()->flash('success', trans('entities.pages_delete_success'));
$this->pageRepo->destroy($page); $this->entityRepo->destroyPage($page);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -355,11 +335,11 @@ class PageController extends Controller
*/ */
public function destroyDraft($bookSlug, $pageId) public function destroyDraft($bookSlug, $pageId)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getById('page', $pageId, true);
$page = $this->pageRepo->getById($pageId, true); $book = $page->book;
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
session()->flash('success', 'Draft deleted'); session()->flash('success', trans('entities.pages_delete_draft_success'));
$this->pageRepo->destroy($page); $this->entityRepo->destroyPage($page);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }
@ -371,10 +351,9 @@ class PageController extends Controller
*/ */
public function showRevisions($bookSlug, $pageSlug) public function showRevisions($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle('Revisions For ' . $page->getShortName()); return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
} }
/** /**
@ -386,16 +365,15 @@ class PageController extends Controller
*/ */
public function showRevision($bookSlug, $pageSlug, $revisionId) public function showRevision($bookSlug, $pageSlug, $revisionId)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $revision = $this->entityRepo->getById('page_revision', $revisionId, false);
$revision = $this->pageRepo->getRevisionById($revisionId);
$page->fill($revision->toArray()); $page->fill($revision->toArray());
$this->setPageTitle('Page Revision For ' . $page->getShortName()); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [ return view('pages/revision', [
'page' => $page, 'page' => $page,
'book' => $book, 'book' => $page->book,
]); ]);
} }
@ -408,20 +386,19 @@ class PageController extends Controller
*/ */
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId) public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id); $revision = $this->entityRepo->getById('page_revision', $revisionId);
$revision = $this->pageRepo->getRevisionById($revisionId);
$prev = $revision->getPrevious(); $prev = $revision->getPrevious();
$prevContent = ($prev === null) ? '' : $prev->html; $prevContent = ($prev === null) ? '' : $prev->html;
$diff = (new Htmldiff)->diff($prevContent, $revision->html); $diff = (new Htmldiff)->diff($prevContent, $revision->html);
$page->fill($revision->toArray()); $page->fill($revision->toArray());
$this->setPageTitle('Page Revision For ' . $page->getShortName()); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [ return view('pages/revision', [
'page' => $page, 'page' => $page,
'book' => $book, 'book' => $page->book,
'diff' => $diff, 'diff' => $diff,
]); ]);
} }
@ -435,11 +412,10 @@ class PageController extends Controller
*/ */
public function restoreRevision($bookSlug, $pageSlug, $revisionId) public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId); $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
Activity::add($page, 'page_restore', $book->id); Activity::add($page, 'page_restore', $page->book->id);
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -452,8 +428,7 @@ class PageController extends Controller
*/ */
public function exportPdf($bookSlug, $pageSlug) public function exportPdf($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$pdfContent = $this->exportService->pageToPdf($page); $pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [ return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream',
@ -469,8 +444,7 @@ class PageController extends Controller
*/ */
public function exportHtml($bookSlug, $pageSlug) public function exportHtml($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToContainedHtml($page); $containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [ return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream',
@ -486,8 +460,7 @@ class PageController extends Controller
*/ */
public function exportPlainText($bookSlug, $pageSlug) public function exportPlainText($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToPlainText($page); $containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [ return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream', 'Content-Type' => 'application/octet-stream',
@ -501,9 +474,9 @@ class PageController extends Controller
*/ */
public function showRecentlyCreated() public function showRecentlyCreated()
{ {
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created')); $pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [ return view('pages/detailed-listing', [
'title' => 'Recently Created Pages', 'title' => trans('entities.recently_created_pages'),
'pages' => $pages 'pages' => $pages
]); ]);
} }
@ -514,9 +487,9 @@ class PageController extends Controller
*/ */
public function showRecentlyUpdated() public function showRecentlyUpdated()
{ {
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated')); $pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [ return view('pages/detailed-listing', [
'title' => 'Recently Updated Pages', 'title' => trans('entities.recently_updated_pages'),
'pages' => $pages 'pages' => $pages
]); ]);
} }
@ -529,8 +502,7 @@ class PageController extends Controller
*/ */
public function showRestrict($bookSlug, $pageSlug) public function showRestrict($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $page); $this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles(); $roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [ return view('pages/restrictions', [
@ -548,11 +520,10 @@ class PageController extends Controller
*/ */
public function showMove($bookSlug, $pageSlug) public function showMove($bookSlug, $pageSlug)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
return view('pages/move', [ return view('pages/move', [
'book' => $book, 'book' => $page->book,
'page' => $page 'page' => $page
]); ]);
} }
@ -567,8 +538,7 @@ class PageController extends Controller
*/ */
public function move($bookSlug, $pageSlug, Request $request) public function move($bookSlug, $pageSlug, Request $request)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null); $entitySelection = $request->get('entity_selection', null);
@ -580,22 +550,17 @@ class PageController extends Controller
$entityType = $stringExploded[0]; $entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]); $entityId = intval($stringExploded[1]);
$parent = false;
if ($entityType == 'chapter') { try {
$parent = $this->chapterRepo->getById($entityId); $parent = $this->entityRepo->getById($entityType, $entityId);
} else if ($entityType == 'book') { } catch (\Exception $e) {
$parent = $this->bookRepo->getById($entityId); session()->flash(trans('entities.selected_book_chapter_not_found'));
}
if ($parent === false || $parent === null) {
session()->flash('The selected Book or Chapter was not found');
return redirect()->back(); return redirect()->back();
} }
$this->pageRepo->changePageParent($page, $parent); $this->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id); Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', sprintf('Page moved to "%s"', $parent->name)); session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl()); return redirect($page->getUrl());
} }
@ -609,11 +574,10 @@ class PageController extends Controller
*/ */
public function restrict($bookSlug, $pageSlug, Request $request) public function restrict($bookSlug, $pageSlug, Request $request)
{ {
$book = $this->bookRepo->getBySlug($bookSlug); $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $page); $this->checkOwnablePermission('restrictions-manage', $page);
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page); $this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
session()->flash('success', 'Page Permissions Updated'); session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl()); return redirect($page->getUrl());
} }

View File

@ -2,9 +2,7 @@
use BookStack\Exceptions\PermissionsException; use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo; use BookStack\Repos\PermissionsRepo;
use BookStack\Services\PermissionService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests;
class PermissionController extends Controller class PermissionController extends Controller
{ {
@ -55,7 +53,7 @@ class PermissionController extends Controller
]); ]);
$this->permissionsRepo->saveNewRole($request->all()); $this->permissionsRepo->saveNewRole($request->all());
session()->flash('success', 'Role successfully created'); session()->flash('success', trans('settings.role_create_success'));
return redirect('/settings/roles'); return redirect('/settings/roles');
} }
@ -69,7 +67,7 @@ class PermissionController extends Controller
{ {
$this->checkPermission('user-roles-manage'); $this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById($id);
if ($role->hidden) throw new PermissionsException('This role cannot be edited'); if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
return view('settings/roles/edit', ['role' => $role]); return view('settings/roles/edit', ['role' => $role]);
} }
@ -88,7 +86,7 @@ class PermissionController extends Controller
]); ]);
$this->permissionsRepo->updateRole($id, $request->all()); $this->permissionsRepo->updateRole($id, $request->all());
session()->flash('success', 'Role successfully updated'); session()->flash('success', trans('settings.role_update_success'));
return redirect('/settings/roles'); return redirect('/settings/roles');
} }
@ -103,7 +101,7 @@ class PermissionController extends Controller
$this->checkPermission('user-roles-manage'); $this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById($id);
$roles = $this->permissionsRepo->getAllRolesExcept($role); $roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole); $roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]); return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
} }
@ -126,7 +124,7 @@ class PermissionController extends Controller
return redirect()->back(); return redirect()->back();
} }
session()->flash('success', 'Role successfully deleted'); session()->flash('success', trans('settings.role_delete_success'));
return redirect('/settings/roles'); return redirect('/settings/roles');
} }
} }

View File

@ -1,34 +1,22 @@
<?php <?php namespace BookStack\Http\Controllers;
namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo;
use BookStack\Services\ViewService; use BookStack\Services\ViewService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
class SearchController extends Controller class SearchController extends Controller
{ {
protected $pageRepo; protected $entityRepo;
protected $bookRepo;
protected $chapterRepo;
protected $viewService; protected $viewService;
/** /**
* SearchController constructor. * SearchController constructor.
* @param PageRepo $pageRepo * @param EntityRepo $entityRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param ViewService $viewService * @param ViewService $viewService
*/ */
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService) public function __construct(EntityRepo $entityRepo, ViewService $viewService)
{ {
$this->pageRepo = $pageRepo; $this->entityRepo = $entityRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->viewService = $viewService; $this->viewService = $viewService;
parent::__construct(); parent::__construct();
} }
@ -46,10 +34,10 @@ class SearchController extends Controller
} }
$searchTerm = $request->get('term'); $searchTerm = $request->get('term');
$paginationAppends = $request->only('term'); $paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends); $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends); $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
$this->setPageTitle('Search For ' . $searchTerm); $this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
return view('search/all', [ return view('search/all', [
'pages' => $pages, 'pages' => $pages,
'books' => $books, 'books' => $books,
@ -69,11 +57,11 @@ class SearchController extends Controller
$searchTerm = $request->get('term'); $searchTerm = $request->get('term');
$paginationAppends = $request->only('term'); $paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends); $pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Page Search For ' . $searchTerm); $this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [ return view('search/entity-search-list', [
'entities' => $pages, 'entities' => $pages,
'title' => 'Page Search Results', 'title' => trans('entities.search_results_page'),
'searchTerm' => $searchTerm 'searchTerm' => $searchTerm
]); ]);
} }
@ -89,11 +77,11 @@ class SearchController extends Controller
$searchTerm = $request->get('term'); $searchTerm = $request->get('term');
$paginationAppends = $request->only('term'); $paginationAppends = $request->only('term');
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends); $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Chapter Search For ' . $searchTerm); $this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [ return view('search/entity-search-list', [
'entities' => $chapters, 'entities' => $chapters,
'title' => 'Chapter Search Results', 'title' => trans('entities.search_results_chapter'),
'searchTerm' => $searchTerm 'searchTerm' => $searchTerm
]); ]);
} }
@ -109,11 +97,11 @@ class SearchController extends Controller
$searchTerm = $request->get('term'); $searchTerm = $request->get('term');
$paginationAppends = $request->only('term'); $paginationAppends = $request->only('term');
$books = $this->bookRepo->getBySearch($searchTerm, 20, $paginationAppends); $books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Book Search For ' . $searchTerm); $this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [ return view('search/entity-search-list', [
'entities' => $books, 'entities' => $books,
'title' => 'Book Search Results', 'title' => trans('entities.search_results_book'),
'searchTerm' => $searchTerm 'searchTerm' => $searchTerm
]); ]);
} }
@ -132,8 +120,8 @@ class SearchController extends Controller
} }
$searchTerm = $request->get('term'); $searchTerm = $request->get('term');
$searchWhereTerms = [['book_id', '=', $bookId]]; $searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->pageRepo->getBySearch($searchTerm, $searchWhereTerms); $pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->chapterRepo->getBySearch($searchTerm, $searchWhereTerms); $chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
} }
@ -152,9 +140,11 @@ class SearchController extends Controller
// Search for entities otherwise show most popular // Search for entities otherwise show most popular
if ($searchTerm !== false) { if ($searchTerm !== false) {
if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items()); foreach (['page', 'chapter', 'book'] as $entityType) {
if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items()); if ($entityTypes->contains($entityType)) {
if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items()); $entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance'); $entities = $entities->sortByDesc('title_relevance');
} else { } else {
$entityNames = $entityTypes->map(function ($type) { $entityNames = $entityTypes->map(function ($type) {

View File

@ -1,8 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use BookStack\Http\Requests;
use Setting; use Setting;
class SettingController extends Controller class SettingController extends Controller
@ -39,7 +38,7 @@ class SettingController extends Controller
Setting::put($key, $value); Setting::put($key, $value);
} }
session()->flash('success', 'Settings Saved'); session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings'); return redirect('/settings');
} }

View File

@ -2,7 +2,6 @@
use BookStack\Repos\TagRepo; use BookStack\Repos\TagRepo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use BookStack\Http\Requests;
class TagController extends Controller class TagController extends Controller
{ {
@ -16,12 +15,14 @@ class TagController extends Controller
public function __construct(TagRepo $tagRepo) public function __construct(TagRepo $tagRepo)
{ {
$this->tagRepo = $tagRepo; $this->tagRepo = $tagRepo;
parent::__construct();
} }
/** /**
* Get all the Tags for a particular entity * Get all the Tags for a particular entity
* @param $entityType * @param $entityType
* @param $entityId * @param $entityId
* @return \Illuminate\Http\JsonResponse
*/ */
public function getForEntity($entityType, $entityId) public function getForEntity($entityType, $entityId)
{ {
@ -29,29 +30,10 @@ class TagController extends Controller
return response()->json($tags); return response()->json($tags);
} }
/**
* Update the tags for a particular entity.
* @param $entityType
* @param $entityId
* @param Request $request
* @return mixed
*/
public function updateForEntity($entityType, $entityId, Request $request)
{
$entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
if ($entity === null) return $this->jsonError("Entity not found", 404);
$inputTags = $request->input('tags');
$tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
return response()->json([
'tags' => $tags,
'message' => 'Tags successfully updated'
]);
}
/** /**
* Get tag name suggestions from a given search term. * Get tag name suggestions from a given search term.
* @param Request $request * @param Request $request
* @return \Illuminate\Http\JsonResponse
*/ */
public function getNameSuggestions(Request $request) public function getNameSuggestions(Request $request)
{ {
@ -63,6 +45,7 @@ class TagController extends Controller
/** /**
* Get tag value suggestions from a given search term. * Get tag value suggestions from a given search term.
* @param Request $request * @param Request $request
* @return \Illuminate\Http\JsonResponse
*/ */
public function getValueSuggestions(Request $request) public function getValueSuggestions(Request $request)
{ {

View File

@ -44,7 +44,7 @@ class UserController extends Controller
'sort' => $request->has('sort') ? $request->get('sort') : 'name', 'sort' => $request->has('sort') ? $request->get('sort') : 'name',
]; ];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails); $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle('Users'); $this->setPageTitle(trans('settings.users'));
$users->appends($listDetails); $users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]); return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
} }
@ -83,7 +83,6 @@ class UserController extends Controller
} }
$this->validate($request, $validationRules); $this->validate($request, $validationRules);
$user = $this->user->fill($request->all()); $user = $this->user->fill($request->all());
if ($authMethod === 'standard') { if ($authMethod === 'standard') {
@ -131,7 +130,7 @@ class UserController extends Controller
$authMethod = ($user->system_name) ? 'system' : config('auth.method'); $authMethod = ($user->system_name) ? 'system' : config('auth.method');
$activeSocialDrivers = $socialAuthService->getActiveDrivers(); $activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle('User Profile'); $this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles(); $roles = $this->userRepo->getAllRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]); return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
} }
@ -154,8 +153,6 @@ class UserController extends Controller
'email' => 'min:2|email|unique:users,email,' . $id, 'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm', 'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password' 'password-confirm' => 'same:password|required_with:password'
], [
'password-confirm.required_with' => 'Password confirmation required'
]); ]);
$user = $this->user->findOrFail($id); $user = $this->user->findOrFail($id);
@ -179,7 +176,7 @@ class UserController extends Controller
} }
$user->save(); $user->save();
session()->flash('success', 'User successfully updated'); session()->flash('success', trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id; $redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
return redirect($redirectUrl); return redirect($redirectUrl);
@ -197,7 +194,7 @@ class UserController extends Controller
}); });
$user = $this->user->findOrFail($id); $user = $this->user->findOrFail($id);
$this->setPageTitle('Delete User ' . $user->name); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]); return view('users/delete', ['user' => $user]);
} }
@ -216,17 +213,17 @@ class UserController extends Controller
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById($id);
if ($this->userRepo->isOnlyAdmin($user)) { if ($this->userRepo->isOnlyAdmin($user)) {
session()->flash('error', 'You cannot delete the only admin'); session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl()); return redirect($user->getEditUrl());
} }
if ($user->system_name === 'public') { if ($user->system_name === 'public') {
session()->flash('error', 'You cannot delete the guest user'); session()->flash('error', trans('errors.users_cannot_delete_guest'));
return redirect($user->getEditUrl()); return redirect($user->getEditUrl());
} }
$this->userRepo->destroy($user); $this->userRepo->destroy($user);
session()->flash('success', 'User successfully removed'); session()->flash('success', trans('settings.users_delete_success'));
return redirect('/settings/users'); return redirect('/settings/users');
} }

View File

@ -4,8 +4,6 @@ namespace BookStack\Http\Middleware;
use Closure; use Closure;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Guard;
use BookStack\Exceptions\UserRegistrationException;
use Setting;
class Authenticate class Authenticate
{ {

View File

@ -43,8 +43,9 @@ class ResetPassword extends Notification
public function toMail() public function toMail()
{ {
return (new MailMessage) return (new MailMessage)
->line('You are receiving this email because we received a password reset request for your account.') ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
->action('Reset Password', baseUrl('password/reset/' . $this->token)) ->line(trans('auth.email_reset_text'))
->line('If you did not request a password reset, no further action is required.'); ->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
->line(trans('auth.email_reset_not_requested'));
} }
} }

View File

@ -7,6 +7,10 @@ class Page extends Entity
protected $simpleAttributes = ['name', 'id', 'slug']; protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
protected $fieldsToSearch = ['name', 'text'];
/** /**
* Converts this page into a simplified array. * Converts this page into a simplified array.
* @return mixed * @return mixed

View File

@ -1,6 +1,8 @@
<?php namespace BookStack\Providers; <?php namespace BookStack\Providers;
use Carbon\Carbon;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Validator;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -12,11 +14,12 @@ class AppServiceProvider extends ServiceProvider
public function boot() public function boot()
{ {
// Custom validation methods // Custom validation methods
\Validator::extend('is_image', function($attribute, $value, $parameters, $validator) { Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp']; $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes); return in_array($value->getMimeType(), $imageMimes);
}); });
Carbon::setLocale(config('app.locale'));
} }
/** /**

View File

@ -1,295 +0,0 @@
<?php namespace BookStack\Repos;
use Alpha\B;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
class BookRepo extends EntityRepo
{
protected $pageRepo;
protected $chapterRepo;
/**
* BookRepo constructor.
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
*/
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
{
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
parent::__construct();
}
/**
* Base query for getting books.
* Takes into account any restrictions.
* @return mixed
*/
private function bookQuery()
{
return $this->permissionService->enforceBookRestrictions($this->book, 'view');
}
/**
* Get the book that has the given id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->bookQuery()->findOrFail($id);
}
/**
* Get all books, Limited by count.
* @param int $count
* @return mixed
*/
public function getAll($count = 10)
{
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
if (!$count) return $bookQuery->get();
return $bookQuery->take($count)->get();
}
/**
* Get all books paginated.
* @param int $count
* @return mixed
*/
public function getAllPaginated($count = 10)
{
return $this->bookQuery()
->orderBy('name', 'asc')->paginate($count);
}
/**
* Get the latest books.
* @param int $count
* @return mixed
*/
public function getLatest($count = 10)
{
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
}
/**
* Gets the most recently viewed for a user.
* @param int $count
* @param int $page
* @return mixed
*/
public function getRecentlyViewed($count = 10, $page = 0)
{
return Views::getUserRecentlyViewed($count, $page, $this->book);
}
/**
* Gets the most viewed books.
* @param int $count
* @param int $page
* @return mixed
*/
public function getPopular($count = 10, $page = 0)
{
return Views::getPopular($count, $page, $this->book);
}
/**
* Get a book by slug
* @param $slug
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug)
{
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) throw new NotFoundException('Book not found');
return $book;
}
/**
* Checks if a book exists.
* @param $id
* @return bool
*/
public function exists($id)
{
return $this->bookQuery()->where('id', '=', $id)->exists();
}
/**
* Get a new book instance from request input.
* @param array $input
* @return Book
*/
public function createFromInput($input)
{
$book = $this->book->newInstance($input);
$book->slug = $this->findSuitableSlug($book->name);
$book->created_by = user()->id;
$book->updated_by = user()->id;
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
return $book;
}
/**
* Update the given book from user input.
* @param Book $book
* @param $input
* @return Book
*/
public function updateFromInput(Book $book, $input)
{
if ($book->name !== $input['name']) {
$book->slug = $this->findSuitableSlug($input['name'], $book->id);
}
$book->fill($input);
$book->updated_by = user()->id;
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
return $book;
}
/**
* Destroy the given book.
* @param Book $book
* @throws \Exception
*/
public function destroy(Book $book)
{
foreach ($book->pages as $page) {
$this->pageRepo->destroy($page);
}
foreach ($book->chapters as $chapter) {
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$book->delete();
}
/**
* Get the next child element priority.
* @param Book $book
* @return int
*/
public function getNewPriority($book)
{
$lastElem = $this->getChildren($book)->pop();
return $lastElem ? $lastElem->priority + 1 : 0;
}
/**
* @param string $slug
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $currentId = false)
{
$query = $this->book->where('slug', '=', $slug);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
/**
* Provides a suitable slug for the given book name.
* Ensures the returned slug is unique in the system.
* @param string $name
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $currentId = false)
{
$slug = $this->nameToSlug($name);
while ($this->doesSlugExist($slug, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @param bool $filterDrafts
* @return mixed
*/
public function getChildren(Book $book, $filterDrafts = false)
{
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
$pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
if ($filterDrafts) {
$pageQuery = $pageQuery->where('draft', '=', false);
}
$pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
$this->permissionService->enforcePageRestrictions($query, 'view');
if ($filterDrafts) $query->where('draft', '=', false);
}]);
$chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
$children = $pages->values();
foreach ($chapters as $chapter) {
$children->push($chapter);
}
$bookSlug = $book->slug;
$children->each(function ($child) use ($bookSlug) {
$child->setAttribute('bookSlug', $bookSlug);
if ($child->isA('chapter')) {
$child->pages->each(function ($page) use ($bookSlug) {
$page->setAttribute('bookSlug', $bookSlug);
});
$child->pages = $child->pages->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->draft) $score -= 100;
return $score;
});
}
});
// Sort items with drafts first then by priority.
return $children->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->isA('page') && $child->draft) $score -= 100;
return $score;
});
}
/**
* Get books by search term.
* @param $term
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
$bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
$books = $bookQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
$book->searchSnippet = $result;
}
return $books;
}
}

View File

@ -1,226 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Chapter;
class ChapterRepo extends EntityRepo
{
protected $pageRepo;
/**
* ChapterRepo constructor.
* @param $pageRepo
*/
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
parent::__construct();
}
/**
* Base query for getting chapters, Takes permissions into account.
* @return mixed
*/
private function chapterQuery()
{
return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
}
/**
* Check if an id exists.
* @param $id
* @return bool
*/
public function idExists($id)
{
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
}
/**
* Get a chapter by a specific id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->chapterQuery()->findOrFail($id);
}
/**
* Get all chapters.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->chapterQuery()->all();
}
/**
* Get a chapter that has the given slug within the given book.
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) throw new NotFoundException('Chapter not found');
return $chapter;
}
/**
* Get the child items for a chapter
* @param Chapter $chapter
*/
public function getChildren(Chapter $chapter)
{
$pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
// Sort items with drafts first then by priority.
return $pages->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->draft) $score -= 100;
return $score;
});
}
/**
* Create a new chapter from request input.
* @param $input
* @param Book $book
* @return Chapter
*/
public function createFromInput($input, Book $book)
{
$chapter = $this->chapter->newInstance($input);
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
$chapter->created_by = user()->id;
$chapter->updated_by = user()->id;
$chapter = $book->chapters()->save($chapter);
$this->permissionService->buildJointPermissionsForEntity($chapter);
return $chapter;
}
/**
* Destroy a chapter and its relations by providing its slug.
* @param Chapter $chapter
*/
public function destroy(Chapter $chapter)
{
if (count($chapter->pages) > 0) {
foreach ($chapter->pages as $page) {
$page->chapter_id = 0;
$page->save();
}
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$chapter->delete();
}
/**
* Check if a chapter's slug exists.
* @param $slug
* @param $bookId
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $bookId, $currentId = false)
{
$query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
/**
* Finds a suitable slug for the provided name.
* Checks database to prevent duplicate slugs.
* @param $name
* @param $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = $this->nameToSlug($name);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Get a new priority value for a new page to be added
* to the given chapter.
* @param Chapter $chapter
* @return int
*/
public function getNewPriority(Chapter $chapter)
{
$lastPage = $chapter->pages->last();
return $lastPage !== null ? $lastPage->priority + 1 : 0;
}
/**
* Get chapters by the given search term.
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
$chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
$chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
$chapter->searchSnippet = $result;
}
return $chapters;
}
/**
* Changes the book relation of this chapter.
* @param $bookId
* @param Chapter $chapter
* @param bool $rebuildPermissions
* @return Chapter
*/
public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
{
$chapter->book_id = $bookId;
// Update related activity
foreach ($chapter->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
$chapter->save();
// Update all child pages
foreach ($chapter->pages as $page) {
$this->pageRepo->changeBook($bookId, $page);
}
// Update permissions if applicable
if ($rebuildPermissions) {
$chapter->load('book');
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
}
return $chapter;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,666 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Services\AttachmentService;
use Carbon\Carbon;
use DOMDocument;
use DOMXPath;
use Illuminate\Support\Str;
use BookStack\Page;
use BookStack\PageRevision;
class PageRepo extends EntityRepo
{
protected $pageRevision;
protected $tagRepo;
/**
* PageRepo constructor.
* @param PageRevision $pageRevision
* @param TagRepo $tagRepo
*/
public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
{
$this->pageRevision = $pageRevision;
$this->tagRepo = $tagRepo;
parent::__construct();
}
/**
* Base query for getting pages, Takes restrictions into account.
* @param bool $allowDrafts
* @return mixed
*/
private function pageQuery($allowDrafts = false)
{
$query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
if (!$allowDrafts) {
$query = $query->where('draft', '=', false);
}
return $query;
}
/**
* Get a page via a specific ID.
* @param $id
* @param bool $allowDrafts
* @return Page
*/
public function getById($id, $allowDrafts = false)
{
return $this->pageQuery($allowDrafts)->findOrFail($id);
}
/**
* Get a page identified by the given slug.
* @param $slug
* @param $bookId
* @return Page
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) throw new NotFoundException('Page not found');
return $page;
}
/**
* Search through page revisions and retrieve
* the last page in the current book that
* has a slug equal to the one given.
* @param $pageSlug
* @param $bookSlug
* @return null | Page
*/
public function findPageUsingOldSlug($pageSlug, $bookSlug)
{
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
->whereHas('page', function ($query) {
$this->permissionService->enforcePageRestrictions($query);
})
->where('type', '=', 'version')
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
->with('page')->first();
return $revision !== null ? $revision->page : null;
}
/**
* Get a new Page instance from the given input.
* @param $input
* @return Page
*/
public function newFromInput($input)
{
$page = $this->page->fill($input);
return $page;
}
/**
* Count the pages with a particular slug within a book.
* @param $slug
* @param $bookId
* @return mixed
*/
public function countBySlug($slug, $bookId)
{
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
* @param Page $draftPage
* @param array $input
* @return Page
*/
public function publishDraft(Page $draftPage, array $input)
{
$draftPage->fill($input);
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
}
$draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->draft = false;
$draftPage->save();
$this->saveRevision($draftPage, 'Initial Publish');
return $draftPage;
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return static
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = 'New Page';
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Parse te headers on the page to get a navigation menu
* @param Page $page
* @return array
*/
public function getPageNav(Page $page)
{
if ($page->html == '') return null;
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
if (is_null($headers)) return null;
$tree = [];
foreach ($headers as $header) {
$text = $header->nodeValue;
$tree[] = [
'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)),
'link' => '#' . $header->getAttribute('id'),
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
];
}
return $tree;
}
/**
* Formats a page's html to be tagged correctly
* within the system.
* @param string $htmlText
* @return string
*/
protected function formatHtml($htmlText)
{
if ($htmlText == '') return $htmlText;
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$idArray = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
$idArray[] = $id;
continue;
};
}
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (in_array($newId, $idArray)) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
}
// Generate inner html as a string
$html = '';
foreach ($childNodes as $childNode) {
$html .= $doc->saveHTML($childNode);
}
return $html;
}
/**
* Gets pages by a search term.
* Highlights page content for showing in results.
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
$pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
$pages = $pageQuery->paginate($count)->appends($paginationAppends);
// Add highlights to page text.
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
//lookahead/behind assertions ensures cut between words
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
foreach ($pages as $page) {
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
//delimiter between occurrences
$results = [];
foreach ($matches as $line) {
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
}
$matchLimit = 6;
if (count($results) > $matchLimit) {
$results = array_slice($results, 0, $matchLimit);
}
$result = join('... ', $results);
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
if (strlen($result) < 5) {
$result = $page->getExcerpt(80);
}
$page->searchSnippet = $result;
}
return $pages;
}
/**
* Search for image usage.
* @param $imageString
* @return mixed
*/
public function searchForImage($imageString)
{
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
$page->text = '';
}
return count($pages) > 0 ? $pages : false;
}
/**
* Updates a page with any fillable data and saves it into the database.
* @param Page $page
* @param int $book_id
* @param string $input
* @return Page
*/
public function updatePage(Page $page, $book_id, $input)
{
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
// Prevent slug being updated if no name change
if ($page->name !== $input['name']) {
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
}
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
}
// Update with new details
$userId = user()->id;
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->updated_by = $userId;
$page->save();
// Remove all update drafts for this user & page.
$this->userUpdateDraftsQuery($page, $userId)->delete();
// Save a revision after updating
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
$this->saveRevision($page, $input['summary']);
}
return $page;
}
/**
* Restores a revision's content back into a page.
* @param Page $page
* @param Book $book
* @param int $revisionId
* @return Page
*/
public function restoreRevision(Page $page, Book $book, $revisionId)
{
$this->saveRevision($page);
$revision = $this->getRevisionById($revisionId);
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
$page->text = strip_tags($page->html);
$page->updated_by = user()->id;
$page->save();
return $page;
}
/**
* Saves a page revision into the system.
* @param Page $page
* @param null|string $summary
* @return $this
*/
public function saveRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->newInstance($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
$revision->created_by = user()->id;
$revision->created_at = $page->updated_at;
$revision->type = 'version';
$revision->summary = $summary;
$revision->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
$this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
}
return $revision;
}
/**
* Save a page update draft.
* @param Page $page
* @param array $data
* @return PageRevision
*/
public function saveUpdateDraft(Page $page, $data = [])
{
$userId = user()->id;
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
if ($drafts->count() > 0) {
$draft = $drafts->first();
} else {
$draft = $this->pageRevision->newInstance();
$draft->page_id = $page->id;
$draft->slug = $page->slug;
$draft->book_slug = $page->book->slug;
$draft->created_by = $userId;
$draft->type = 'update_draft';
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
$draft->save();
return $draft;
}
/**
* Update a draft page.
* @param Page $page
* @param array $data
* @return Page
*/
public function updateDraftPage(Page $page, $data = [])
{
$page->fill($data);
if (isset($data['html'])) {
$page->text = strip_tags($data['html']);
}
$page->save();
return $page;
}
/**
* The base query for getting user update drafts.
* @param Page $page
* @param $userId
* @return mixed
*/
private function userUpdateDraftsQuery(Page $page, $userId)
{
return $this->pageRevision->where('created_by', '=', $userId)
->where('type', 'update_draft')
->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc');
}
/**
* Checks whether a user has a draft version of a particular page or not.
* @param Page $page
* @param $userId
* @return bool
*/
public function hasUserGotPageDraft(Page $page, $userId)
{
return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
}
/**
* Get the latest updated draft revision for a particular page and user.
* @param Page $page
* @param $userId
* @return mixed
*/
public function getUserPageDraft(Page $page, $userId)
{
return $this->userUpdateDraftsQuery($page, $userId)->first();
}
/**
* Get the notification message that informs the user that they are editing a draft page.
* @param PageRevision $draft
* @return string
*/
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
$message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
}
return $message;
}
/**
* Check if a page is being actively editing.
* Checks for edits since last page updated.
* Passing in a minuted range will check for edits
* within the last x minutes.
* @param Page $page
* @param null $minRange
* @return bool
*/
public function isPageEditingActive(Page $page, $minRange = null)
{
$draftSearch = $this->activePageEditingQuery($page, $minRange);
return $draftSearch->count() > 0;
}
/**
* Get a notification message concerning the editing activity on
* a particular page.
* @param Page $page
* @param null $minRange
* @return string
*/
public function getPageEditingActiveMessage(Page $page, $minRange = null)
{
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
$userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
$timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
$message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
return sprintf($message, $userMessage, $timeMessage);
}
/**
* A query to check for active update drafts on a particular page.
* @param Page $page
* @param null $minRange
* @return mixed
*/
private function activePageEditingQuery(Page $page, $minRange = null)
{
$query = $this->pageRevision->where('type', '=', 'update_draft')
->where('page_id', '=', $page->id)
->where('updated_at', '>', $page->updated_at)
->where('created_by', '!=', user()->id)
->with('createdBy');
if ($minRange !== null) {
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
}
return $query;
}
/**
* Gets a single revision via it's id.
* @param $id
* @return PageRevision
*/
public function getRevisionById($id)
{
return $this->pageRevision->findOrFail($id);
}
/**
* Checks if a slug exists within a book already.
* @param $slug
* @param $bookId
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $bookId, $currentId = false)
{
$query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
if ($currentId) $query = $query->where('id', '!=', $currentId);
return $query->count() > 0;
}
/**
* Changes the related book for the specified page.
* Changes the book id of any relations to the page that store the book id.
* @param int $bookId
* @param Page $page
* @return Page
*/
public function changeBook($bookId, Page $page)
{
$page->book_id = $bookId;
foreach ($page->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
$page->save();
return $page;
}
/**
* Change the page's parent to the given entity.
* @param Page $page
* @param Entity $parent
*/
public function changePageParent(Page $page, Entity $parent)
{
$book = $parent->isA('book') ? $parent : $parent->book;
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
$page->save();
$page = $this->changeBook($book->id, $page);
$page->load('book');
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Gets a suitable slug for the resource
* @param string $name
* @param int $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = $this->nameToSlug($name);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Destroy a given page along with its dependencies.
* @param $page
*/
public function destroy(Page $page)
{
Activity::removeEntity($page);
$page->views()->delete();
$page->tags()->delete();
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
// Delete AttachedFiles
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {
$attachmentService->deleteFile($attachment);
}
$page->delete();
}
/**
* Get the latest pages added to the system.
* @param $count
* @return mixed
*/
public function getRecentlyCreatedPaginated($count = 20)
{
return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
}
/**
* Get the latest pages added to the system.
* @param $count
* @return mixed
*/
public function getRecentlyUpdatedPaginated($count = 20)
{
return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
}
}

View File

@ -133,9 +133,9 @@ class PermissionsRepo
// Prevent deleting admin role or default registration role. // Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) { if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
throw new PermissionsException('This role is a system role and cannot be deleted'); throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
} else if ($role->id == setting('registration-role')) { } else if ($role->id == setting('registration-role')) {
throw new PermissionsException('This role cannot be deleted while set as the default registration role.'); throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
} }
if ($migrateRoleId) { if ($migrateRoleId) {

View File

@ -38,7 +38,7 @@ class TagRepo
{ {
$entityInstance = $this->entity->getEntityInstance($entityType); $entityInstance = $this->entity->getEntityInstance($entityType);
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags'); $searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
$searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action); $searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
return $searchQuery->first(); return $searchQuery->first();
} }
@ -121,7 +121,7 @@ class TagRepo
/** /**
* Create a new Tag instance from user input. * Create a new Tag instance from user input.
* @param $input * @param $input
* @return static * @return Tag
*/ */
protected function newInstanceFromInput($input) protected function newInstanceFromInput($input)
{ {

View File

@ -3,7 +3,6 @@
use BookStack\Role; use BookStack\Role;
use BookStack\User; use BookStack\User;
use Exception; use Exception;
use Setting;
class UserRepo class UserRepo
{ {
@ -169,13 +168,13 @@ class UserRepo
public function getRecentlyCreated(User $user, $count = 20) public function getRecentlyCreated(User $user, $count = 20)
{ {
return [ return [
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) { 'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id); $query->where('created_by', '=', $user->id);
}), }),
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) { 'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id); $query->where('created_by', '=', $user->id);
}), }),
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) { 'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id); $query->where('created_by', '=', $user->id);
}) })
]; ];

View File

@ -114,7 +114,7 @@ class ActivityService
$activity = $this->permissionService $activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type') ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get(); ->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activity); return $this->filterSimilar($activity);
} }

View File

@ -193,7 +193,7 @@ class AttachmentService extends UploadService
try { try {
$storage->put($attachmentStoragePath, $attachmentData); $storage->put($attachmentStoragePath, $attachmentData);
} catch (Exception $e) { } catch (Exception $e) {
throw new FileUploadException('File path ' . $attachmentStoragePath . ' could not be uploaded to. Ensure it is writable to the server.'); throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
} }
return $attachmentPath; return $attachmentPath;
} }

View File

@ -33,7 +33,7 @@ class EmailConfirmationService
public function sendConfirmation(User $user) public function sendConfirmation(User $user)
{ {
if ($user->email_confirmed) { if ($user->email_confirmed) {
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login'); throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
} }
$this->deleteConfirmationsByUser($user); $this->deleteConfirmationsByUser($user);
@ -63,7 +63,7 @@ class EmailConfirmationService
* Gets an email confirmation by looking up the token, * Gets an email confirmation by looking up the token,
* Ensures the token has not expired. * Ensures the token has not expired.
* @param string $token * @param string $token
* @return EmailConfirmation * @return array|null|\stdClass
* @throws UserRegistrationException * @throws UserRegistrationException
*/ */
public function getEmailConfirmationFromToken($token) public function getEmailConfirmationFromToken($token)
@ -72,14 +72,14 @@ class EmailConfirmationService
// If not found show error // If not found show error
if ($emailConfirmation === null) { if ($emailConfirmation === null) {
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register'); throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
} }
// If more than a day old // If more than a day old
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) { if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
$user = $this->users->getById($emailConfirmation->user_id); $user = $this->users->getById($emailConfirmation->user_id);
$this->sendConfirmation($user); $this->sendConfirmation($user);
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm'); throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
} }
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id); $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Services; <?php namespace BookStack\Services;
use BookStack\Page; use BookStack\Page;
class ExportService class ExportService
@ -28,8 +27,13 @@ class ExportService
{ {
$cssContent = file_get_contents(public_path('/css/export-styles.css')); $cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render(); $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
$useWKHTML = config('snappy.pdf.binary') !== false;
$containedHtml = $this->containHtml($pageHtml); $containedHtml = $this->containHtml($pageHtml);
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
} else {
$pdf = \PDF::loadHTML($containedHtml); $pdf = \PDF::loadHTML($containedHtml);
}
return $pdf->output(); return $pdf->output();
} }

View File

@ -59,7 +59,7 @@ class ImageService extends UploadService
{ {
$imageName = $imageName ? $imageName : basename($url); $imageName = $imageName ? $imageName : basename($url);
$imageData = file_get_contents($url); $imageData = file_get_contents($url);
if($imageData === false) throw new \Exception('Cannot get image from ' . $url); if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
return $this->saveNew($imageName, $imageData, $type); return $this->saveNew($imageName, $imageData, $type);
} }
@ -93,7 +93,7 @@ class ImageService extends UploadService
$storage->put($fullPath, $imageData); $storage->put($fullPath, $imageData);
$storage->setVisibility($fullPath, 'public'); $storage->setVisibility($fullPath, 'public');
} catch (Exception $e) { } catch (Exception $e) {
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
} }
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath); if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
@ -160,7 +160,7 @@ class ImageService extends UploadService
$thumb = $this->imageTool->make($storage->get($imagePath)); $thumb = $this->imageTool->make($storage->get($imagePath));
} catch (Exception $e) { } catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.'); throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
} else { } else {
throw $e; throw $e;
} }

View File

@ -94,7 +94,7 @@ class LdapService
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
} }
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details')); if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
} }
/** /**
@ -109,7 +109,7 @@ class LdapService
// Check LDAP extension in installed // Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException('LDAP PHP extension not installed'); throw new LdapException(trans('errors.ldap_extension_not_installed'));
} }
// Get port from server string if specified. // Get port from server string if specified.
@ -117,7 +117,7 @@ class LdapService
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389); $ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
if ($ldapConnection === false) { if ($ldapConnection === false) {
throw new LdapException('Cannot connect to ldap server, Initial connection failed'); throw new LdapException(trans('errors.ldap_cannot_connect'));
} }
// Set any required options // Set any required options

View File

@ -8,8 +8,9 @@ use BookStack\Ownable;
use BookStack\Page; use BookStack\Page;
use BookStack\Role; use BookStack\Role;
use BookStack\User; use BookStack\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class PermissionService class PermissionService
{ {
@ -23,6 +24,8 @@ class PermissionService
public $chapter; public $chapter;
public $page; public $page;
protected $db;
protected $jointPermission; protected $jointPermission;
protected $role; protected $role;
@ -31,18 +34,21 @@ class PermissionService
/** /**
* PermissionService constructor. * PermissionService constructor.
* @param JointPermission $jointPermission * @param JointPermission $jointPermission
* @param Connection $db
* @param Book $book * @param Book $book
* @param Chapter $chapter * @param Chapter $chapter
* @param Page $page * @param Page $page
* @param Role $role * @param Role $role
*/ */
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role) public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{ {
$this->db = $db;
$this->jointPermission = $jointPermission; $this->jointPermission = $jointPermission;
$this->role = $role; $this->role = $role;
$this->book = $book; $this->book = $book;
$this->chapter = $chapter; $this->chapter = $chapter;
$this->page = $page; $this->page = $page;
// TODO - Update so admin still goes through filters
} }
/** /**
@ -302,6 +308,10 @@ class PermissionService
$explodedAction = explode('-', $action); $explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction); $restrictionAction = end($explodedAction);
if ($role->system_name === 'admin') {
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
}
if ($entity->isA('book')) { if ($entity->isA('book')) {
if (!$entity->restricted) { if (!$entity->restricted) {
@ -461,14 +471,61 @@ class PermissionService
return $q; return $q;
} }
public function bookChildrenQuery($book_id, $filterDrafts = false) {
// Draft setup
$params = [
'userId' => $this->currentUser()->id,
'bookIdPage' => $book_id,
'bookIdChapter' => $book_id
];
if (!$filterDrafts) {
$params['userIdDrafts'] = $this->currentUser()->id;
}
// Role setup
$userRoles = $this->getRoles();
$roleBindings = [];
$roleValues = [];
foreach ($userRoles as $index => $roleId) {
$roleBindings[':role'.$index] = $roleId;
$roleValues['role'.$index] = $roleId;
}
// TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
// Something which will handle the above role crap in a nice clean way
$roleBindingString = implode(',', array_keys($roleBindings));
$query = "SELECT * from (
(SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
UNION
(SELECT 'Bookstack\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft FROM {$this->chapter->getTable()} WHERE book_id = :bookIdChapter)
) as U WHERE (
SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
WHERE
jp.entity_id=U.id AND
jp.entity_type=U.entity_type AND
jp.action = 'view' AND
jp.role_id IN ({$roleBindingString}) AND
(
jp.has_permission = 1 OR
(jp.has_permission_own = 1 AND jp.created_by = :userId)
)
) > 0
ORDER BY draft desc, priority asc";
$this->clean();
return $this->db->select($query, array_replace($roleValues, $params));
}
/** /**
* Add restrictions for a page query * Add restrictions for a generic entity
* @param $query * @param string $entityType
* @param Builder|Entity $query
* @param string $action * @param string $action
* @return mixed * @return mixed
*/ */
public function enforcePageRestrictions($query, $action = 'view') public function enforceEntityRestrictions($entityType, $query, $action = 'view')
{ {
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others. // Prevent drafts being visible to others.
$query = $query->where(function ($query) { $query = $query->where(function ($query) {
$query->where('draft', '=', false); $query->where('draft', '=', false);
@ -478,44 +535,13 @@ class PermissionService
}); });
} }
}); });
return $this->enforceEntityRestrictions($query, $action);
} }
/**
* Add on permission restrictions to a chapter query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceChapterRestrictions($query, $action = 'view')
{
return $this->enforceEntityRestrictions($query, $action);
}
/**
* Add restrictions to a book query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceBookRestrictions($query, $action = 'view')
{
return $this->enforceEntityRestrictions($query, $action);
}
/**
* Add restrictions for a generic entity
* @param $query
* @param string $action
* @return mixed
*/
public function enforceEntityRestrictions($query, $action = 'view')
{
if ($this->isAdmin()) { if ($this->isAdmin()) {
$this->clean(); $this->clean();
return $query; return $query;
} }
$this->currentAction = $action; $this->currentAction = $action;
return $this->entityRestrictionQuery($query); return $this->entityRestrictionQuery($query);
} }
@ -601,7 +627,7 @@ class PermissionService
private function isAdmin() private function isAdmin()
{ {
if ($this->isAdminUser === null) { if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false; $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
} }
return $this->isAdminUser; return $this->isAdminUser;

View File

@ -70,12 +70,12 @@ class SocialAuthService
// Check social account has not already been used // Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) { if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login'); throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
} }
if ($this->userRepo->getByEmail($socialUser->getEmail())) { if ($this->userRepo->getByEmail($socialUser->getEmail())) {
$email = $socialUser->getEmail(); $email = $socialUser->getEmail();
throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login'); throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
} }
return $socialUser; return $socialUser;
@ -98,7 +98,6 @@ class SocialAuthService
// Get any attached social accounts or users // Get any attached social accounts or users
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first(); $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$user = $this->userRepo->getByEmail($socialUser->getEmail());
$isLoggedIn = auth()->check(); $isLoggedIn = auth()->check();
$currentUser = user(); $currentUser = user();
@ -113,27 +112,26 @@ class SocialAuthService
if ($isLoggedIn && $socialAccount === null) { if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser); $this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount); $currentUser->socialAccounts()->save($this->socialAccount);
session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.'); session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl()); return redirect($currentUser->getEditUrl());
} }
// When a user is logged in and the social account exists and is already linked to the current user. // When a user is logged in and the social account exists and is already linked to the current user.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.'); session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl()); return redirect($currentUser->getEditUrl());
} }
// When a user is logged in, A social account exists but the users do not match. // When a user is logged in, A social account exists but the users do not match.
// Change the user that the social account is assigned to.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.'); session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl()); return redirect($currentUser->getEditUrl());
} }
// Otherwise let the user know this social account is not used by anyone. // Otherwise let the user know this social account is not used by anyone.
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings'; $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
if (setting('registration-enabled')) { if (setting('registration-enabled')) {
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option'; $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
} }
throw new SocialSignInException($message . '.', '/login'); throw new SocialSignInException($message . '.', '/login');
@ -157,8 +155,8 @@ class SocialAuthService
{ {
$driver = trim(strtolower($socialDriver)); $driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found'); if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly."); if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
return $driver; return $driver;
} }
@ -215,7 +213,7 @@ class SocialAuthService
{ {
session(); session();
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete(); user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
session()->flash('success', title_case($socialDriver) . ' account successfully detached'); session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl()); return redirect(user()->getEditUrl());
} }

View File

@ -5,9 +5,7 @@ use BookStack\View;
class ViewService class ViewService
{ {
protected $view; protected $view;
protected $user;
protected $permissionService; protected $permissionService;
/** /**
@ -18,7 +16,6 @@ class ViewService
public function __construct(View $view, PermissionService $permissionService) public function __construct(View $view, PermissionService $permissionService)
{ {
$this->view = $view; $this->view = $view;
$this->user = user();
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
} }
@ -29,8 +26,9 @@ class ViewService
*/ */
public function add(Entity $entity) public function add(Entity $entity)
{ {
if ($this->user === null) return 0; $user = user();
$view = $entity->views()->where('user_id', '=', $this->user->id)->first(); if ($user === null || $user->isDefault()) return 0;
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists // Add view if model exists
if ($view) { if ($view) {
$view->increment('views'); $view->increment('views');
@ -39,7 +37,7 @@ class ViewService
// Otherwise create new view count // Otherwise create new view count
$entity->views()->save($this->view->create([ $entity->views()->save($this->view->create([
'user_id' => $this->user->id, 'user_id' => $user->id,
'views' => 1 'views' => 1
])); ]));
@ -78,13 +76,14 @@ class ViewService
*/ */
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{ {
if ($this->user === null) return collect(); $user = user();
if ($user === null || $user->isDefault()) return collect();
$query = $this->permissionService $query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', user()->id); $query = $query->where('user_id', '=', $user->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc') $viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable'); ->skip($count * $page)->take($count)->get()->pluck('viewable');

View File

@ -74,6 +74,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->roles->pluck('name')->contains($role); return $this->roles->pluck('name')->contains($role);
} }
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasSystemRole($role)
{
return $this->roles->pluck('system_name')->contains('admin');
}
/** /**
* Get all permissions belonging to a the current user. * Get all permissions belonging to a the current user.
* @param bool $cache * @param bool $cache

View File

@ -15,7 +15,8 @@
"league/flysystem-aws-s3-v3": "^1.0", "league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.7", "barryvdh/laravel-dompdf": "^0.7",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"gathercontent/htmldiff": "^0.2.1" "gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1"
}, },
"require-dev": { "require-dev": {
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",

121
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "3124d900cfe857392a94de479f3ff6d4", "hash": "2438a2f4a02adbea5f378f9e9408eb29",
"content-hash": "a968767a73f77e66e865c276cf76eedf", "content-hash": "6add8bff71ecc86e0c90858590834a26",
"packages": [ "packages": [
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
@ -255,6 +255,58 @@
], ],
"time": "2016-07-04 11:52:48" "time": "2016-07-04 11:52:48"
}, },
{
"name": "barryvdh/laravel-snappy",
"version": "v0.3.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-snappy.git",
"reference": "509a4497be63d8ee7ff464a3daf00d9edde08e21"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/509a4497be63d8ee7ff464a3daf00d9edde08e21",
"reference": "509a4497be63d8ee7ff464a3daf00d9edde08e21",
"shasum": ""
},
"require": {
"illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x",
"illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x",
"knplabs/knp-snappy": "*",
"php": ">=5.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.3-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\Snappy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Snappy PDF/Image for Laravel 4",
"keywords": [
"image",
"laravel",
"pdf",
"snappy",
"wkhtmltoimage",
"wkhtmltopdf"
],
"time": "2016-08-05 13:08:28"
},
{ {
"name": "barryvdh/reflection-docblock", "name": "barryvdh/reflection-docblock",
"version": "v2.0.4", "version": "v2.0.4",
@ -997,6 +1049,71 @@
], ],
"time": "2015-12-05 17:17:57" "time": "2015-12-05 17:17:57"
}, },
{
"name": "knplabs/knp-snappy",
"version": "0.4.3",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/snappy.git",
"reference": "44f7a9b37d5686fd7db4c1e9569a802a5d16923f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/KnpLabs/snappy/zipball/44f7a9b37d5686fd7db4c1e9569a802a5d16923f",
"reference": "44f7a9b37d5686fd7db4c1e9569a802a5d16923f",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"symfony/process": "~2.3|~3.0"
},
"require-dev": {
"phpunit/phpunit": "~4.7"
},
"suggest": {
"h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
"h4cc/wkhtmltoimage-i386": "Provides wkhtmltoimage-i386 binary for Linux-compatible machines, use version `~0.12` as dependency",
"h4cc/wkhtmltopdf-amd64": "Provides wkhtmltopdf-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
"h4cc/wkhtmltopdf-i386": "Provides wkhtmltopdf-i386 binary for Linux-compatible machines, use version `~0.12` as dependency",
"wemersonjanuario/wkhtmltopdf-windows": "Provides wkhtmltopdf executable for Windows, use version `~0.12` as dependency"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.5.x-dev"
}
},
"autoload": {
"psr-0": {
"Knp\\Snappy": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "KNPLabs Team",
"homepage": "http://knplabs.com"
},
{
"name": "Symfony Community",
"homepage": "http://github.com/KnpLabs/snappy/contributors"
}
],
"description": "PHP5 library allowing thumbnail, snapshot or PDF generation from a url or a html page. Wrapper for wkhtmltopdf/wkhtmltoimage.",
"homepage": "http://github.com/KnpLabs/snappy",
"keywords": [
"knp",
"knplabs",
"pdf",
"snapshot",
"thumbnail",
"wkhtmltopdf"
],
"time": "2015-11-17 13:16:27"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v5.3.11", "version": "v5.3.11",

View File

@ -148,6 +148,7 @@ return [
Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class, Barryvdh\Debugbar\ServiceProvider::class,
Barryvdh\Snappy\ServiceProvider::class,
/* /*
@ -218,6 +219,7 @@ return [
'ImageTool' => Intervention\Image\Facades\Image::class, 'ImageTool' => Intervention\Image\Facades\Image::class,
'PDF' => Barryvdh\DomPDF\Facade::class, 'PDF' => Barryvdh\DomPDF\Facade::class,
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class, 'Debugbar' => Barryvdh\Debugbar\Facade::class,
/** /**

View File

@ -6,6 +6,7 @@
return [ return [
'app-name' => 'BookStack', 'app-name' => 'BookStack',
'app-logo' => '',
'app-name-header' => true, 'app-name-header' => true,
'app-editor' => 'wysiwyg', 'app-editor' => 'wysiwyg',
'app-color' => '#0288D1', 'app-color' => '#0288D1',

18
config/snappy.php Normal file
View File

@ -0,0 +1,18 @@
<?php
return [
'pdf' => [
'enabled' => true,
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
'timeout' => false,
'options' => [],
'env' => [],
],
'image' => [
'enabled' => false,
'binary' => '/usr/local/bin/wkhtmltoimage',
'timeout' => false,
'options' => [],
'env' => [],
],
];

View File

@ -1,8 +1,9 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"prod": "gulp --production", "build": "gulp --production",
"dev": "gulp watch" "dev": "gulp watch",
"watch": "gulp watch"
}, },
"devDependencies": { "devDependencies": {
"angular": "^1.5.5", "angular": "^1.5.5",

View File

@ -22,6 +22,7 @@
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/> <env name="APP_DEBUG" value="false"/>
<env name="APP_LANG" value="en"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/> <env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/> <env name="QUEUE_DRIVER" value="sync"/>

View File

@ -17,22 +17,33 @@ A platform for storing and organising information and documentation. General inf
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements: All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
* [Node.js](https://nodejs.org/en/) * [Node.js](https://nodejs.org/en/) v6.9+
* [Gulp](http://gulpjs.com/)
SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. To run the build task you can use the following commands:
``` bash
# Build and minify for production
npm run-script build
# Build for dev (With sourcemaps) and watch for changes
npm run-script dev
```
BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing. BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.
The testing database will also need migrating and seeding beforehand. This can be done with the following commands: The testing database will also need migrating and seeding beforehand. This can be done with the following commands:
``` ``` bash
php artisan migrate --database=mysql_testing php artisan migrate --database=mysql_testing
php artisan db:seed --class=DummyContentSeeder --database=mysql_testing php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
``` ```
Once done you can run `phpunit` in the application root directory to run all tests. Once done you can run `phpunit` in the application root directory to run all tests.
## Website and Docs
The website and project docs are currently stored in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. The docs are stored as markdown files in the `resources/docs` folder
## License ## License
BookStack is provided under the MIT License. BookStack is provided under the MIT License.
@ -53,5 +64,11 @@ These are the great projects used to help build BookStack:
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [Marked](https://github.com/chjj/marked) * [Marked](https://github.com/chjj/marked)
* [Moment.js](http://momentjs.com/) * [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
* [Dompdf](https://github.com/barryvdh/laravel-dompdf)
* [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy)
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy. Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy.

View File

@ -2,6 +2,8 @@
import moment from 'moment'; import moment from 'moment';
import 'moment/locale/en-gb'; import 'moment/locale/en-gb';
import editorOptions from "./pages/page-form";
moment.locale('en-gb'); moment.locale('en-gb');
export default function (ngApp, events) { export default function (ngApp, events) {
@ -23,14 +25,14 @@ export default function (ngApp, events) {
$scope.searching = false; $scope.searching = false;
$scope.searchTerm = ''; $scope.searchTerm = '';
var page = 0; let page = 0;
var previousClickTime = 0; let previousClickTime = 0;
var previousClickImage = 0; let previousClickImage = 0;
var dataLoaded = false; let dataLoaded = false;
var callback = false; let callback = false;
var preSearchImages = []; let preSearchImages = [];
var preSearchHasMore = false; let preSearchHasMore = false;
/** /**
* Used by dropzone to get the endpoint to upload to. * Used by dropzone to get the endpoint to upload to.
@ -62,7 +64,7 @@ export default function (ngApp, events) {
$scope.$apply(() => { $scope.$apply(() => {
$scope.images.unshift(data); $scope.images.unshift(data);
}); });
events.emit('success', 'Image uploaded'); events.emit('success', trans('components.image_upload_success'));
}; };
/** /**
@ -79,9 +81,9 @@ export default function (ngApp, events) {
* @param image * @param image
*/ */
$scope.imageSelect = function (image) { $scope.imageSelect = function (image) {
var dblClickTime = 300; let dblClickTime = 300;
var currentTime = Date.now(); let currentTime = Date.now();
var timeDiff = currentTime - previousClickTime; let timeDiff = currentTime - previousClickTime;
if (timeDiff < dblClickTime && image.id === previousClickImage) { if (timeDiff < dblClickTime && image.id === previousClickImage) {
// If double click // If double click
@ -137,22 +139,21 @@ export default function (ngApp, events) {
$('#image-manager').find('.overlay').fadeOut(240); $('#image-manager').find('.overlay').fadeOut(240);
}; };
var baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/'); let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
/** /**
* Fetch the list image data from the server. * Fetch the list image data from the server.
*/ */
function fetchData() { function fetchData() {
var url = baseUrl + page + '?'; let url = baseUrl + page + '?';
var components = {}; let components = {};
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo; if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
if ($scope.searching) components['term'] = $scope.searchTerm; if ($scope.searching) components['term'] = $scope.searchTerm;
var urlQueryString = Object.keys(components).map((key) => { url += Object.keys(components).map((key) => {
return key + '=' + encodeURIComponent(components[key]); return key + '=' + encodeURIComponent(components[key]);
}).join('&'); }).join('&');
url += urlQueryString;
$http.get(url).then((response) => { $http.get(url).then((response) => {
$scope.images = $scope.images.concat(response.data.images); $scope.images = $scope.images.concat(response.data.images);
@ -205,13 +206,13 @@ export default function (ngApp, events) {
*/ */
$scope.saveImageDetails = function (event) { $scope.saveImageDetails = function (event) {
event.preventDefault(); event.preventDefault();
var url = window.baseUrl('/images/update/' + $scope.selectedImage.id); let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
$http.put(url, this.selectedImage).then(response => { $http.put(url, this.selectedImage).then(response => {
events.emit('success', 'Image details updated'); events.emit('success', trans('components.image_update_success'));
}, (response) => { }, (response) => {
if (response.status === 422) { if (response.status === 422) {
var errors = response.data; let errors = response.data;
var message = ''; let message = '';
Object.keys(errors).forEach((key) => { Object.keys(errors).forEach((key) => {
message += errors[key].join('\n'); message += errors[key].join('\n');
}); });
@ -230,13 +231,13 @@ export default function (ngApp, events) {
*/ */
$scope.deleteImage = function (event) { $scope.deleteImage = function (event) {
event.preventDefault(); event.preventDefault();
var force = $scope.dependantPages !== false; let force = $scope.dependantPages !== false;
var url = window.baseUrl('/images/' + $scope.selectedImage.id); let url = window.baseUrl('/images/' + $scope.selectedImage.id);
if (force) url += '?force=true'; if (force) url += '?force=true';
$http.delete(url).then((response) => { $http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false; $scope.selectedImage = false;
events.emit('success', 'Image successfully deleted'); events.emit('success', trans('components.image_delete_success'));
}, (response) => { }, (response) => {
// Pages failure // Pages failure
if (response.status === 400) { if (response.status === 400) {
@ -266,11 +267,11 @@ export default function (ngApp, events) {
$scope.searchBook = function (e) { $scope.searchBook = function (e) {
e.preventDefault(); e.preventDefault();
var term = $scope.searchTerm; let term = $scope.searchTerm;
if (term.length == 0) return; if (term.length == 0) return;
$scope.searching = true; $scope.searching = true;
$scope.searchResults = ''; $scope.searchResults = '';
var searchUrl = window.baseUrl('/search/book/' + $attrs.bookId); let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
searchUrl += '?term=' + encodeURIComponent(term); searchUrl += '?term=' + encodeURIComponent(term);
$http.get(searchUrl).then((response) => { $http.get(searchUrl).then((response) => {
$scope.searchResults = $sce.trustAsHtml(response.data); $scope.searchResults = $sce.trustAsHtml(response.data);
@ -294,27 +295,27 @@ export default function (ngApp, events) {
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) { function ($scope, $http, $attrs, $interval, $timeout, $sce) {
$scope.editorOptions = require('./pages/page-form'); $scope.editorOptions = editorOptions();
$scope.editContent = ''; $scope.editContent = '';
$scope.draftText = ''; $scope.draftText = '';
var pageId = Number($attrs.pageId); let pageId = Number($attrs.pageId);
var isEdit = pageId !== 0; let isEdit = pageId !== 0;
var autosaveFrequency = 30; // AutoSave interval in seconds. let autosaveFrequency = 30; // AutoSave interval in seconds.
var isMarkdown = $attrs.editorType === 'markdown'; let isMarkdown = $attrs.editorType === 'markdown';
$scope.draftsEnabled = $attrs.draftsEnabled === 'true'; $scope.draftsEnabled = $attrs.draftsEnabled === 'true';
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1; $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1; $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
// Set initial header draft text // Set initial header draft text
if ($scope.isUpdateDraft || $scope.isNewPageDraft) { if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
$scope.draftText = 'Editing Draft' $scope.draftText = trans('entities.pages_editing_draft');
} else { } else {
$scope.draftText = 'Editing Page' $scope.draftText = trans('entities.pages_editing_page');
} }
var autoSave = false; let autoSave = false;
var currentContent = { let currentContent = {
title: false, title: false,
html: false html: false
}; };
@ -351,8 +352,8 @@ export default function (ngApp, events) {
autoSave = $interval(() => { autoSave = $interval(() => {
// Return if manually saved recently to prevent bombarding the server // Return if manually saved recently to prevent bombarding the server
if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return; if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return;
var newTitle = $('#name').val(); let newTitle = $('#name').val();
var newHtml = $scope.editContent; let newHtml = $scope.editContent;
if (newTitle !== currentContent.title || newHtml !== currentContent.html) { if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml; currentContent.html = newHtml;
@ -369,7 +370,7 @@ export default function (ngApp, events) {
*/ */
function saveDraft() { function saveDraft() {
if (!$scope.draftsEnabled) return; if (!$scope.draftsEnabled) return;
var data = { let data = {
name: $('#name').val(), name: $('#name').val(),
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
}; };
@ -379,14 +380,14 @@ export default function (ngApp, events) {
let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft'); let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
$http.put(url, data).then(responseData => { $http.put(url, data).then(responseData => {
draftErroring = false; draftErroring = false;
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate(); let updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm'); $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
showDraftSaveNotification(); showDraftSaveNotification();
lastSave = Date.now(); lastSave = Date.now();
}, errorRes => { }, errorRes => {
if (draftErroring) return; if (draftErroring) return;
events.emit('error', 'Failed to save draft. Ensure you have internet connection before saving this page.') events.emit('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true; draftErroring = true;
}); });
} }
@ -419,7 +420,7 @@ export default function (ngApp, events) {
let url = window.baseUrl('/ajax/page/' + pageId); let url = window.baseUrl('/ajax/page/' + pageId);
$http.get(url).then((responseData) => { $http.get(url).then((responseData) => {
if (autoSave) $interval.cancel(autoSave); if (autoSave) $interval.cancel(autoSave);
$scope.draftText = 'Editing Page'; $scope.draftText = trans('entities.pages_editing_page');
$scope.isUpdateDraft = false; $scope.isUpdateDraft = false;
$scope.$broadcast('html-update', responseData.data.html); $scope.$broadcast('html-update', responseData.data.html);
$scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html); $scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
@ -427,7 +428,7 @@ export default function (ngApp, events) {
$timeout(() => { $timeout(() => {
startAutoSave(); startAutoSave();
}, 1000); }, 1000);
events.emit('success', 'Draft discarded, The editor has been updated with the current page content'); events.emit('success', trans('entities.pages_draft_discarded'));
}); });
}; };
@ -505,20 +506,6 @@ export default function (ngApp, events) {
} }
}; };
/**
* Save the tags to the current page.
*/
$scope.saveTags = function() {
setTagOrder();
let postData = {tags: $scope.tags};
let url = window.baseUrl('/ajax/tags/update/page/' + pageId);
$http.post(url, postData).then((responseData) => {
$scope.tags = responseData.data.tags;
addEmptyTag();
events.emit('success', responseData.data.message);
})
};
/** /**
* Remove a tag from the current list. * Remove a tag from the current list.
* @param tag * @param tag
@ -588,7 +575,7 @@ export default function (ngApp, events) {
* Get files for the current page from the server. * Get files for the current page from the server.
*/ */
function getFiles() { function getFiles() {
let url = window.baseUrl(`/attachments/get/page/${pageId}`) let url = window.baseUrl(`/attachments/get/page/${pageId}`);
$http.get(url).then(resp => { $http.get(url).then(resp => {
$scope.files = resp.data; $scope.files = resp.data;
currentOrder = resp.data.map(file => {return file.id}).join(':'); currentOrder = resp.data.map(file => {return file.id}).join(':');
@ -606,7 +593,7 @@ export default function (ngApp, events) {
$scope.$apply(() => { $scope.$apply(() => {
$scope.files.push(data); $scope.files.push(data);
}); });
events.emit('success', 'File uploaded'); events.emit('success', trans('entities.attachments_file_uploaded'));
}; };
/** /**
@ -624,7 +611,7 @@ export default function (ngApp, events) {
data.link = ''; data.link = '';
} }
}); });
events.emit('success', 'File updated'); events.emit('success', trans('entities.attachments_file_updated'));
}; };
/** /**
@ -650,7 +637,7 @@ export default function (ngApp, events) {
file.uploaded_to = pageId; file.uploaded_to = pageId;
$http.post(window.baseUrl('/attachments/link'), file).then(resp => { $http.post(window.baseUrl('/attachments/link'), file).then(resp => {
$scope.files.push(resp.data); $scope.files.push(resp.data);
events.emit('success', 'Link attached'); events.emit('success', trans('entities.attachments_link_attached'));
$scope.file = getCleanFile(); $scope.file = getCleanFile();
}, checkError('link')); }, checkError('link'));
}; };
@ -684,7 +671,7 @@ export default function (ngApp, events) {
$scope.editFile.link = ''; $scope.editFile.link = '';
} }
$scope.editFile = false; $scope.editFile = false;
events.emit('success', 'Attachment details updated'); events.emit('success', trans('entities.attachments_updated_success'));
}, checkError('edit')); }, checkError('edit'));
}; };

View File

@ -1,38 +1,8 @@
"use strict"; "use strict";
const DropZone = require('dropzone'); import DropZone from "dropzone";
const markdown = require('marked'); import markdown from "marked";
module.exports = function (ngApp, events) { export default function (ngApp, events) {
/**
* Toggle Switches
* Has basic on/off functionality.
* Use string values of 'true' & 'false' to dictate the current state.
*/
ngApp.directive('toggleSwitch', function () {
return {
restrict: 'A',
template: `
<div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
<input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
<div class="switch-handle"></div>
</div>
`,
scope: true,
link: function (scope, element, attrs) {
scope.name = attrs.name;
scope.value = attrs.value;
scope.isActive = scope.value == true && scope.value != 'false';
scope.value = (scope.value == true && scope.value != 'false') ? 'true' : 'false';
scope.switch = function () {
scope.isActive = !scope.isActive;
scope.value = scope.isActive ? 'true' : 'false';
}
}
};
});
/** /**
* Common tab controls using simple jQuery functions. * Common tab controls using simple jQuery functions.
@ -65,7 +35,7 @@ module.exports = function (ngApp, events) {
}); });
/** /**
* Sub form component to allow inner-form sections to act like thier own forms. * Sub form component to allow inner-form sections to act like their own forms.
*/ */
ngApp.directive('subForm', function() { ngApp.directive('subForm', function() {
return { return {
@ -80,96 +50,13 @@ module.exports = function (ngApp, events) {
element.find('button[type="submit"]').click(submitEvent); element.find('button[type="submit"]').click(submitEvent);
function submitEvent(e) { function submitEvent(e) {
e.preventDefault() e.preventDefault();
if (attrs.subForm) scope.$eval(attrs.subForm); if (attrs.subForm) scope.$eval(attrs.subForm);
} }
} }
}; };
}); });
/**
* Image Picker
* Is a simple front-end interface that connects to an ImageManager if present.
*/
ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) {
return {
restrict: 'E',
template: `
<div class="image-picker">
<div>
<img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
<img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
</div>
<button class="button" type="button" ng-click="showImageManager()">Select Image</button>
<br>
<button class="text-button" ng-click="reset()" type="button">Reset</button>
<span ng-show="showRemove" class="sep">|</span>
<button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
<input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
</div>
`,
scope: {
name: '@',
resizeHeight: '@',
resizeWidth: '@',
resizeCrop: '@',
showRemove: '=',
currentImage: '@',
currentId: '@',
defaultImage: '@',
imageClass: '@'
},
link: function (scope, element, attrs) {
let usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
scope.image = scope.currentImage;
scope.value = scope.currentImage || '';
if (usingIds) scope.value = scope.currentId;
function setImage(imageModel, imageUrl) {
scope.image = imageUrl;
scope.value = usingIds ? imageModel.id : imageUrl;
}
scope.reset = function () {
setImage({id: 0}, scope.defaultImage);
};
scope.remove = function () {
scope.image = 'none';
scope.value = 'none';
};
scope.showImageManager = function () {
imageManagerService.show((image) => {
scope.updateImageFromModel(image);
});
};
scope.updateImageFromModel = function (model) {
let isResized = scope.resizeWidth && scope.resizeHeight;
if (!isResized) {
scope.$apply(() => {
setImage(model, model.url);
});
return;
}
let cropped = scope.resizeCrop ? 'true' : 'false';
let requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped;
requestString = window.baseUrl(requestString);
$http.get(requestString).then((response) => {
setImage(model, response.data.url);
});
};
}
};
}]);
/** /**
* DropZone * DropZone
* Used for uploading images * Used for uploading images
@ -179,25 +66,26 @@ module.exports = function (ngApp, events) {
restrict: 'E', restrict: 'E',
template: ` template: `
<div class="dropzone-container"> <div class="dropzone-container">
<div class="dz-message">Drop files or click here to upload</div> <div class="dz-message">{{message}}</div>
</div> </div>
`, `,
scope: { scope: {
uploadUrl: '@', uploadUrl: '@',
eventSuccess: '=', eventSuccess: '=',
eventError: '=', eventError: '=',
uploadedTo: '@' uploadedTo: '@',
}, },
link: function (scope, element, attrs) { link: function (scope, element, attrs) {
scope.message = attrs.message;
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder; if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { let dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
url: scope.uploadUrl, url: scope.uploadUrl,
init: function () { init: function () {
var dz = this; let dz = this;
dz.on('sending', function (file, xhr, data) { dz.on('sending', function (file, xhr, data) {
var token = window.document.querySelector('meta[name=token]').getAttribute('content'); let token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token); data.append('_token', token);
var uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo; let uploadedTo = typeof scope.uploadedTo === 'undefined' ? 0 : scope.uploadedTo;
data.append('uploaded_to', uploadedTo); data.append('uploaded_to', uploadedTo);
}); });
if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess); if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess);
@ -214,7 +102,7 @@ module.exports = function (ngApp, events) {
$(file.previewElement).find('[data-dz-errormessage]').text(message); $(file.previewElement).find('[data-dz-errormessage]').text(message);
} }
if (xhr.status === 413) setMessage('The server does not allow uploads of this size. Please try a smaller file.'); if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
if (errorMessage.file) setMessage(errorMessage.file[0]); if (errorMessage.file) setMessage(errorMessage.file[0]);
}); });
@ -273,7 +161,7 @@ module.exports = function (ngApp, events) {
function tinyMceSetup(editor) { function tinyMceSetup(editor) {
editor.on('ExecCommand change NodeChange ObjectResized', (e) => { editor.on('ExecCommand change NodeChange ObjectResized', (e) => {
var content = editor.getContent(); let content = editor.getContent();
$timeout(() => { $timeout(() => {
scope.mceModel = content; scope.mceModel = content;
}); });
@ -301,9 +189,9 @@ module.exports = function (ngApp, events) {
// Custom tinyMCE plugins // Custom tinyMCE plugins
tinymce.PluginManager.add('customhr', function (editor) { tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () { editor.addCommand('InsertHorizontalRule', function () {
var hrElem = document.createElement('hr'); let hrElem = document.createElement('hr');
var cNode = editor.selection.getNode(); let cNode = editor.selection.getNode();
var parentNode = cNode.parentNode; let parentNode = cNode.parentNode;
parentNode.insertBefore(hrElem, cNode); parentNode.insertBefore(hrElem, cNode);
}); });
@ -373,15 +261,21 @@ module.exports = function (ngApp, events) {
link: function (scope, element, attrs) { link: function (scope, element, attrs) {
// Elements // Elements
const input = element.find('[markdown-input] textarea').first(); const $input = element.find('[markdown-input] textarea').first();
const display = element.find('.markdown-display').first(); const $display = element.find('.markdown-display').first();
const insertImage = element.find('button[data-action="insertImage"]'); const $insertImage = element.find('button[data-action="insertImage"]');
const insertEntityLink = element.find('button[data-action="insertEntityLink"]') const $insertEntityLink = element.find('button[data-action="insertEntityLink"]');
// Prevent markdown display link click redirect
$display.on('click', 'a', function(event) {
event.preventDefault();
window.open(this.getAttribute('href'));
});
let currentCaretPos = 0; let currentCaretPos = 0;
input.blur(event => { $input.blur(event => {
currentCaretPos = input[0].selectionStart; currentCaretPos = $input[0].selectionStart;
}); });
// Scroll sync // Scroll sync
@ -391,10 +285,10 @@ module.exports = function (ngApp, events) {
displayHeight; displayHeight;
function setScrollHeights() { function setScrollHeights() {
inputScrollHeight = input[0].scrollHeight; inputScrollHeight = $input[0].scrollHeight;
inputHeight = input.height(); inputHeight = $input.height();
displayScrollHeight = display[0].scrollHeight; displayScrollHeight = $display[0].scrollHeight;
displayHeight = display.height(); displayHeight = $display.height();
} }
setTimeout(() => { setTimeout(() => {
@ -403,29 +297,29 @@ module.exports = function (ngApp, events) {
window.addEventListener('resize', setScrollHeights); window.addEventListener('resize', setScrollHeights);
let scrollDebounceTime = 800; let scrollDebounceTime = 800;
let lastScroll = 0; let lastScroll = 0;
input.on('scroll', event => { $input.on('scroll', event => {
let now = Date.now(); let now = Date.now();
if (now - lastScroll > scrollDebounceTime) { if (now - lastScroll > scrollDebounceTime) {
setScrollHeights() setScrollHeights()
} }
let scrollPercent = (input.scrollTop() / (inputScrollHeight - inputHeight)); let scrollPercent = ($input.scrollTop() / (inputScrollHeight - inputHeight));
let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent; let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent;
display.scrollTop(displayScrollY); $display.scrollTop(displayScrollY);
lastScroll = now; lastScroll = now;
}); });
// Editor key-presses // Editor key-presses
input.keydown(event => { $input.keydown(event => {
// Insert image shortcut // Insert image shortcut
if (event.which === 73 && event.ctrlKey && event.shiftKey) { if (event.which === 73 && event.ctrlKey && event.shiftKey) {
event.preventDefault(); event.preventDefault();
let caretPos = input[0].selectionStart; let caretPos = $input[0].selectionStart;
let currentContent = input.val(); let currentContent = $input.val();
const mdImageText = "![](http://)"; const mdImageText = "![](http://)";
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); $input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
input.focus(); $input.focus();
input[0].selectionStart = caretPos + ("![](".length); $input[0].selectionStart = caretPos + ("![](".length);
input[0].selectionEnd = caretPos + ('![](http://'.length); $input[0].selectionEnd = caretPos + ('![](http://'.length);
return; return;
} }
@ -440,48 +334,48 @@ module.exports = function (ngApp, events) {
}); });
// Insert image from image manager // Insert image from image manager
insertImage.click(event => { $insertImage.click(event => {
window.ImageManager.showExternal(image => { window.ImageManager.showExternal(image => {
let caretPos = currentCaretPos; let caretPos = currentCaretPos;
let currentContent = input.val(); let currentContent = $input.val();
let mdImageText = "![" + image.name + "](" + image.thumbs.display + ")"; let mdImageText = "![" + image.name + "](" + image.thumbs.display + ")";
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); $input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
input.change(); $input.change();
}); });
}); });
function showLinkSelector() { function showLinkSelector() {
window.showEntityLinkSelector((entity) => { window.showEntityLinkSelector((entity) => {
let selectionStart = currentCaretPos; let selectionStart = currentCaretPos;
let selectionEnd = input[0].selectionEnd; let selectionEnd = $input[0].selectionEnd;
let textSelected = (selectionEnd !== selectionStart); let textSelected = (selectionEnd !== selectionStart);
let currentContent = input.val(); let currentContent = $input.val();
if (textSelected) { if (textSelected) {
let selectedText = currentContent.substring(selectionStart, selectionEnd); let selectedText = currentContent.substring(selectionStart, selectionEnd);
let linkText = `[${selectedText}](${entity.link})`; let linkText = `[${selectedText}](${entity.link})`;
input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd)); $input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd));
} else { } else {
let linkText = ` [${entity.name}](${entity.link}) `; let linkText = ` [${entity.name}](${entity.link}) `;
input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart)) $input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart))
} }
input.change(); $input.change();
}); });
} }
insertEntityLink.click(showLinkSelector); $insertEntityLink.click(showLinkSelector);
// Upload and insert image on paste // Upload and insert image on paste
function editorPaste(e) { function editorPaste(e) {
e = e.originalEvent; e = e.originalEvent;
if (!e.clipboardData) return if (!e.clipboardData) return
var items = e.clipboardData.items; let items = e.clipboardData.items;
if (!items) return; if (!items) return;
for (var i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
uploadImage(items[i].getAsFile()); uploadImage(items[i].getAsFile());
} }
} }
input.on('paste', editorPaste); $input.on('paste', editorPaste);
// Handle image drop, Uploads images to BookStack. // Handle image drop, Uploads images to BookStack.
function handleImageDrop(event) { function handleImageDrop(event) {
@ -493,17 +387,17 @@ module.exports = function (ngApp, events) {
} }
} }
input.on('drop', handleImageDrop); $input.on('drop', handleImageDrop);
// Handle image upload and add image into markdown content // Handle image upload and add image into markdown content
function uploadImage(file) { function uploadImage(file) {
if (file.type.indexOf('image') !== 0) return; if (file.type.indexOf('image') !== 0) return;
var formData = new FormData(); let formData = new FormData();
var ext = 'png'; let ext = 'png';
var xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
if (file.name) { if (file.name) {
var fileNameMatches = file.name.match(/\.(.+)$/); let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches) { if (fileNameMatches) {
ext = fileNameMatches[1]; ext = fileNameMatches[1];
} }
@ -511,17 +405,17 @@ module.exports = function (ngApp, events) {
// Insert image into markdown // Insert image into markdown
let id = "image-" + Math.random().toString(16).slice(2); let id = "image-" + Math.random().toString(16).slice(2);
let selectStart = input[0].selectionStart; let selectStart = $input[0].selectionStart;
let selectEnd = input[0].selectionEnd; let selectEnd = $input[0].selectionEnd;
let content = input[0].value; let content = $input[0].value;
let selectText = content.substring(selectStart, selectEnd); let selectText = content.substring(selectStart, selectEnd);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`); let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let innerContent = ((selectEnd > selectStart) ? `![${selectText}]` : '![]') + `(${placeholderImage})`; let innerContent = ((selectEnd > selectStart) ? `![${selectText}]` : '![]') + `(${placeholderImage})`;
input[0].value = content.substring(0, selectStart) + innerContent + content.substring(selectEnd); $input[0].value = content.substring(0, selectStart) + innerContent + content.substring(selectEnd);
input.focus(); $input.focus();
input[0].selectionStart = selectStart; $input[0].selectionStart = selectStart;
input[0].selectionEnd = selectStart; $input[0].selectionEnd = selectStart;
let remoteFilename = "image-" + Date.now() + "." + ext; let remoteFilename = "image-" + Date.now() + "." + ext;
formData.append('file', file, remoteFilename); formData.append('file', file, remoteFilename);
@ -529,20 +423,20 @@ module.exports = function (ngApp, events) {
xhr.open('POST', window.baseUrl('/images/gallery/upload')); xhr.open('POST', window.baseUrl('/images/gallery/upload'));
xhr.onload = function () { xhr.onload = function () {
let selectStart = input[0].selectionStart; let selectStart = $input[0].selectionStart;
if (xhr.status === 200 || xhr.status === 201) { if (xhr.status === 200 || xhr.status === 201) {
var result = JSON.parse(xhr.responseText); let result = JSON.parse(xhr.responseText);
input[0].value = input[0].value.replace(placeholderImage, result.thumbs.display); $input[0].value = $input[0].value.replace(placeholderImage, result.thumbs.display);
input.change(); $input.change();
} else { } else {
console.log('An error occurred uploading the image'); console.log(trans('errors.image_upload_error'));
console.log(xhr.responseText); console.log(xhr.responseText);
input[0].value = input[0].value.replace(innerContent, ''); $input[0].value = $input[0].value.replace(innerContent, '');
input.change(); $input.change();
} }
input.focus(); $input.focus();
input[0].selectionStart = selectStart; $input[0].selectionStart = selectStart;
input[0].selectionEnd = selectStart; $input[0].selectionEnd = selectStart;
}; };
xhr.send(formData); xhr.send(formData);
} }
@ -680,8 +574,7 @@ module.exports = function (ngApp, events) {
} }
// Enter or tab key // Enter or tab key
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) { else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
let text = suggestionElems[active].textContent; currentInput[0].value = suggestionElems[active].textContent;
currentInput[0].value = text;
currentInput.focus(); currentInput.focus();
$suggestionBox.hide(); $suggestionBox.hide();
isShowing = false; isShowing = false;
@ -732,14 +625,13 @@ module.exports = function (ngApp, events) {
// Build suggestions // Build suggestions
$suggestionBox[0].innerHTML = ''; $suggestionBox[0].innerHTML = '';
for (let i = 0; i < suggestions.length; i++) { for (let i = 0; i < suggestions.length; i++) {
var suggestion = document.createElement('li'); let suggestion = document.createElement('li');
suggestion.textContent = suggestions[i]; suggestion.textContent = suggestions[i];
suggestion.onclick = suggestionClick; suggestion.onclick = suggestionClick;
if (i === 0) { if (i === 0) {
suggestion.className = 'active' suggestion.className = 'active';
active = 0; active = 0;
} }
;
$suggestionBox[0].appendChild(suggestion); $suggestionBox[0].appendChild(suggestion);
} }
@ -748,12 +640,11 @@ module.exports = function (ngApp, events) {
// Suggestion click event // Suggestion click event
function suggestionClick(event) { function suggestionClick(event) {
let text = this.textContent; currentInput[0].value = this.textContent;
currentInput[0].value = text;
currentInput.focus(); currentInput.focus();
$suggestionBox.hide(); $suggestionBox.hide();
isShowing = false; isShowing = false;
}; }
// Get suggestions & cache // Get suggestions & cache
function getSuggestions(input, url) { function getSuggestions(input, url) {
@ -779,7 +670,7 @@ module.exports = function (ngApp, events) {
ngApp.directive('entityLinkSelector', [function($http) { ngApp.directive('entityLinkSelector', [function($http) {
return { return {
restict: 'A', restrict: 'A',
link: function(scope, element, attrs) { link: function(scope, element, attrs) {
const selectButton = element.find('.entity-link-selector-confirm'); const selectButton = element.find('.entity-link-selector-confirm');
@ -843,7 +734,7 @@ module.exports = function (ngApp, events) {
const input = element.find('[entity-selector-input]').first(); const input = element.find('[entity-selector-input]').first();
// Detect double click events // Detect double click events
var lastClick = 0; let lastClick = 0;
function isDoubleClick() { function isDoubleClick() {
let now = Date.now(); let now = Date.now();
let answer = now - lastClick < 300; let answer = now - lastClick < 300;

View File

@ -1,11 +1,11 @@
"use strict"; "use strict";
// AngularJS - Create application and load components // AngularJS - Create application and load components
var angular = require('angular'); import angular from "angular";
var ngResource = require('angular-resource'); import "angular-resource";
var ngAnimate = require('angular-animate'); import "angular-animate";
var ngSanitize = require('angular-sanitize'); import "angular-sanitize";
require('angular-ui-sortable'); import "angular-ui-sortable";
// Url retrieval function // Url retrieval function
window.baseUrl = function(path) { window.baseUrl = function(path) {
@ -15,7 +15,13 @@ window.baseUrl = function(path) {
return basePath + '/' + path; return basePath + '/' + path;
}; };
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
import Translations from "./translations"
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
// Global Event System // Global Event System
class EventManager { class EventManager {
@ -25,9 +31,9 @@ class EventManager {
emit(eventName, eventData) { emit(eventName, eventData) {
if (typeof this.listeners[eventName] === 'undefined') return this; if (typeof this.listeners[eventName] === 'undefined') return this;
var eventsToStart = this.listeners[eventName]; let eventsToStart = this.listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) { for (let i = 0; i < eventsToStart.length; i++) {
var event = eventsToStart[i]; let event = eventsToStart[i];
event(eventData); event(eventData);
} }
return this; return this;
@ -70,49 +76,47 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
}); });
// Global jQuery Elements // Global jQuery Elements
$(function () { let notifications = $('.notification');
let successNotification = notifications.filter('.pos');
var notifications = $('.notification'); let errorNotification = notifications.filter('.neg');
var successNotification = notifications.filter('.pos'); let warningNotification = notifications.filter('.warning');
var errorNotification = notifications.filter('.neg'); // Notification Events
var warningNotification = notifications.filter('.warning'); window.Events.listen('success', function (text) {
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide(); successNotification.hide();
successNotification.find('span').text(text); successNotification.find('span').text(text);
setTimeout(() => { setTimeout(() => {
successNotification.show(); successNotification.show();
}, 1); }, 1);
}); });
window.Events.listen('warning', function (text) { window.Events.listen('warning', function (text) {
warningNotification.find('span').text(text); warningNotification.find('span').text(text);
warningNotification.show(); warningNotification.show();
}); });
window.Events.listen('error', function (text) { window.Events.listen('error', function (text) {
errorNotification.find('span').text(text); errorNotification.find('span').text(text);
errorNotification.show(); errorNotification.show();
}); });
// Notification hiding // Notification hiding
notifications.click(function () { notifications.click(function () {
$(this).fadeOut(100); $(this).fadeOut(100);
}); });
// Chapter page list toggles // Chapter page list toggles
$('.chapter-toggle').click(function (e) { $('.chapter-toggle').click(function (e) {
e.preventDefault(); e.preventDefault();
$(this).toggleClass('open'); $(this).toggleClass('open');
$(this).closest('.chapter').find('.inset-list').slideToggle(180); $(this).closest('.chapter').find('.inset-list').slideToggle(180);
}); });
// Back to top button // Back to top button
$('#back-to-top').click(function() { $('#back-to-top').click(function() {
$('#header').smoothScrollTo(); $('#header').smoothScrollTo();
}); });
var scrollTopShowing = false; let scrollTopShowing = false;
var scrollTop = document.getElementById('back-to-top'); let scrollTop = document.getElementById('back-to-top');
var scrollTopBreakpoint = 1200; let scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() { window.addEventListener('scroll', function() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0; let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) { if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
scrollTop.style.display = 'block'; scrollTop.style.display = 'block';
@ -127,36 +131,28 @@ $(function () {
scrollTop.style.display = 'none'; scrollTop.style.display = 'none';
}, 500); }, 500);
} }
}); });
// Common jQuery actions // Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(function() { $('[data-action="expand-entity-list-details"]').click(function() {
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240); $('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
}); });
// Popup close // Popup close
$('.popup-close').click(function() { $('.popup-close').click(function() {
$(this).closest('.overlay').fadeOut(240); $(this).closest('.overlay').fadeOut(240);
}); });
$('.overlay').click(function(event) { $('.overlay').click(function(event) {
if (!$(event.target).hasClass('overlay')) return; if (!$(event.target).hasClass('overlay')) return;
$(this).fadeOut(240); $(this).fadeOut(240);
}); });
// Prevent markdown display link click redirect // Detect IE for css
$('.markdown-display').on('click', 'a', function(event) { if(navigator.userAgent.indexOf('MSIE')!==-1
event.preventDefault();
window.open($(this).attr('href'));
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0 || navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){ || navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support'); $('body').addClass('flexbox-support');
} }
});
// Page specific items // Page specific items
require('./pages/page-show'); import "./pages/page-show";

View File

@ -60,7 +60,8 @@ function registerEditorShortcuts(editor) {
editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']); editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
} }
var mceOptions = module.exports = { export default function() {
let settings = {
selector: '#html-editor', selector: '#html-editor',
content_css: [ content_css: [
window.baseUrl('/css/styles.css'), window.baseUrl('/css/styles.css'),
@ -147,21 +148,20 @@ var mceOptions = module.exports = {
// Run additional setup actions // Run additional setup actions
// Used by the angular side of things // Used by the angular side of things
for (let i = 0; i < mceOptions.extraSetups.length; i++) { for (let i = 0; i < settings.extraSetups.length; i++) {
mceOptions.extraSetups[i](editor); settings.extraSetups[i](editor);
} }
registerEditorShortcuts(editor); registerEditorShortcuts(editor);
(function () { let wrap;
var wrap;
function hasTextContent(node) { function hasTextContent(node) {
return node && !!( node.textContent || node.innerText ); return node && !!( node.textContent || node.innerText );
} }
editor.on('dragstart', function () { editor.on('dragstart', function () {
var node = editor.selection.getNode(); let node = editor.selection.getNode();
if (node.nodeName !== 'IMG') return; if (node.nodeName !== 'IMG') return;
wrap = editor.dom.getParent(node, '.mceTemp'); wrap = editor.dom.getParent(node, '.mceTemp');
@ -172,7 +172,7 @@ var mceOptions = module.exports = {
}); });
editor.on('drop', function (event) { editor.on('drop', function (event) {
var dom = editor.dom, let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc()); rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
// Don't allow anything to be dropped in a captioned image. // Don't allow anything to be dropped in a captioned image.
@ -190,7 +190,6 @@ var mceOptions = module.exports = {
wrap = null; wrap = null;
}); });
})();
// Custom Image picker button // Custom Image picker button
editor.addButton('image-insert', { editor.addButton('image-insert', {
@ -212,4 +211,6 @@ var mceOptions = module.exports = {
editorPaste(event, editor); editorPaste(event, editor);
}); });
} }
}; };
return settings;
}

View File

@ -1,16 +1,13 @@
"use strict"; "use strict";
// Configure ZeroClipboard // Configure ZeroClipboard
var zeroClipBoard = require('zeroclipboard'); import zeroClipBoard from "zeroclipboard";
zeroClipBoard.config({
swfPath: window.baseUrl('/ZeroClipboard.swf')
});
window.setupPageShow = module.exports = function (pageId) { export default window.setupPageShow = function (pageId) {
// Set up pointer // Set up pointer
var $pointer = $('#pointer').detach(); let $pointer = $('#pointer').detach();
var $pointerInner = $pointer.children('div.pointer').first(); let $pointerInner = $pointer.children('div.pointer').first();
var isSelection = false; let isSelection = false;
// Select all contents on input click // Select all contents on input click
$pointer.on('click', 'input', function (e) { $pointer.on('click', 'input', function (e) {
@ -19,6 +16,9 @@ window.setupPageShow = module.exports = function (pageId) {
}); });
// Set up copy-to-clipboard // Set up copy-to-clipboard
zeroClipBoard.config({
swfPath: window.baseUrl('/ZeroClipboard.swf')
});
new zeroClipBoard($pointer.find('button').first()[0]); new zeroClipBoard($pointer.find('button').first()[0]);
// Hide pointer when clicking away // Hide pointer when clicking away
@ -31,11 +31,11 @@ window.setupPageShow = module.exports = function (pageId) {
// Show pointer when selecting a single block of tagged content // Show pointer when selecting a single block of tagged content
$('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) { $('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
e.stopPropagation(); e.stopPropagation();
var selection = window.getSelection(); let selection = window.getSelection();
if (selection.toString().length === 0) return; if (selection.toString().length === 0) return;
// Show pointer and set link // Show pointer and set link
var $elem = $(this); let $elem = $(this);
let link = window.baseUrl('/link/' + pageId + '#' + $elem.attr('id')); let link = window.baseUrl('/link/' + pageId + '#' + $elem.attr('id'));
if (link.indexOf('http') !== 0) link = window.location.protocol + "//" + window.location.host + link; if (link.indexOf('http') !== 0) link = window.location.protocol + "//" + window.location.host + link;
$pointer.find('input').val(link); $pointer.find('input').val(link);
@ -44,9 +44,9 @@ window.setupPageShow = module.exports = function (pageId) {
$pointer.show(); $pointer.show();
// Set pointer to sit near mouse-up position // Set pointer to sit near mouse-up position
var pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2)); let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
if (pointerLeftOffset < 0) pointerLeftOffset = 0; if (pointerLeftOffset < 0) pointerLeftOffset = 0;
var pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100; let pointerLeftOffsetPercent = (pointerLeftOffset / $elem.width()) * 100;
$pointerInner.css('left', pointerLeftOffsetPercent + '%'); $pointerInner.css('left', pointerLeftOffsetPercent + '%');
isSelection = true; isSelection = true;
@ -57,7 +57,7 @@ window.setupPageShow = module.exports = function (pageId) {
// Go to, and highlight if necessary, the specified text. // Go to, and highlight if necessary, the specified text.
function goToText(text) { function goToText(text) {
var idElem = $('.page-content #' + text).first(); let idElem = $('.page-content #' + text).first();
if (idElem.length !== 0) { if (idElem.length !== 0) {
idElem.smoothScrollTo(); idElem.smoothScrollTo();
idElem.css('background-color', 'rgba(244, 249, 54, 0.25)'); idElem.css('background-color', 'rgba(244, 249, 54, 0.25)');
@ -68,19 +68,19 @@ window.setupPageShow = module.exports = function (pageId) {
// Check the hash on load // Check the hash on load
if (window.location.hash) { if (window.location.hash) {
var text = window.location.hash.replace(/\%20/g, ' ').substr(1); let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
goToText(text); goToText(text);
} }
// Make the book-tree sidebar stick in view on scroll // Make the book-tree sidebar stick in view on scroll
var $window = $(window); let $window = $(window);
var $bookTree = $(".book-tree"); let $bookTree = $(".book-tree");
var $bookTreeParent = $bookTree.parent(); let $bookTreeParent = $bookTree.parent();
// Check the page is scrollable and the content is taller than the tree // Check the page is scrollable and the content is taller than the tree
var pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height()); let pageScrollable = ($(document).height() > $window.height()) && ($bookTree.height() < $('.page-content').height());
// Get current tree's width and header height // Get current tree's width and header height
var headerHeight = $("#header").height() + $(".toolbar").height(); let headerHeight = $("#header").height() + $(".toolbar").height();
var isFixed = $window.scrollTop() > headerHeight; let isFixed = $window.scrollTop() > headerHeight;
// Function to fix the tree as a sidebar // Function to fix the tree as a sidebar
function stickTree() { function stickTree() {
$bookTree.width($bookTreeParent.width() + 15); $bookTree.width($bookTreeParent.width() + 15);
@ -95,7 +95,7 @@ window.setupPageShow = module.exports = function (pageId) {
} }
// Checks if the tree stickiness state should change // Checks if the tree stickiness state should change
function checkTreeStickiness(skipCheck) { function checkTreeStickiness(skipCheck) {
var shouldBeFixed = $window.scrollTop() > headerHeight; let shouldBeFixed = $window.scrollTop() > headerHeight;
if (shouldBeFixed && (!isFixed || skipCheck)) { if (shouldBeFixed && (!isFixed || skipCheck)) {
stickTree(); stickTree();
} else if (!shouldBeFixed && (isFixed || skipCheck)) { } else if (!shouldBeFixed && (isFixed || skipCheck)) {

View File

@ -0,0 +1,47 @@
/**
* Translation Manager
* Handles the JavaScript side of translating strings
* in a way which fits with Laravel.
*/
class Translator {
/**
* Create an instance, Passing in the required translations
* @param translations
*/
constructor(translations) {
this.store = translations;
}
/**
* Get a translation, Same format as laravel's 'trans' helper
* @param key
* @param replacements
* @returns {*}
*/
get(key, replacements) {
let splitKey = key.split('.');
let value = splitKey.reduce((a, b) => {
return a != undefined ? a[b] : a;
}, this.store);
if (value === undefined) {
console.log(`Translation with key "${key}" does not exist`);
value = key;
}
if (replacements === undefined) return value;
let replaceMatches = value.match(/:([\S]+)/g);
if (replaceMatches === null) return value;
replaceMatches.forEach(match => {
let key = match.substring(1);
if (typeof replacements[key] === 'undefined') return;
value = value.replace(match, replacements[key]);
});
return value;
}
}
export default Translator

View File

@ -466,3 +466,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
} }
} }
} }
.image-picker .none {
display: none;
}

View File

@ -33,7 +33,7 @@
position: relative; position: relative;
z-index: 5; z-index: 5;
textarea { textarea {
font-family: 'Roboto Mono'; font-family: 'Roboto Mono', monospace;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
padding: $-xs $-m; padding: $-xs $-m;
@ -55,6 +55,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid #DDD; border: 1px solid #DDD;
width: 50%;
} }
.markdown-display { .markdown-display {
padding: 0 $-m 0; padding: 0 $-m 0;
@ -68,7 +69,7 @@
.editor-toolbar { .editor-toolbar {
width: 100%; width: 100%;
padding: $-xs $-m; padding: $-xs $-m;
font-family: 'Roboto Mono'; font-family: 'Roboto Mono', monospace;
font-size: 11px; font-size: 11px;
line-height: 1.6; line-height: 1.6;
border-bottom: 1px solid #DDD; border-bottom: 1px solid #DDD;
@ -268,8 +269,3 @@ input.outline {
.image-picker img { .image-picker img {
background-color: #BBB; background-color: #BBB;
} }
div[toggle-switch] {
height: 18px;
width: 150px;
}

View File

@ -322,6 +322,9 @@ ul.pagination {
font-size: 0.75em; font-size: 0.75em;
margin-top: $-xs; margin-top: $-xs;
} }
.text-muted p.text-muted {
margin-top: 0;
}
.page.draft .text-page { .page.draft .text-page {
color: $color-page-draft; color: $color-page-draft;
} }

View File

@ -35,6 +35,12 @@ table.table {
tr:hover { tr:hover {
background-color: #EEE; background-color: #EEE;
} }
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
} }
table.no-style { table.no-style {

View File

@ -109,6 +109,9 @@ em, i, .italic {
small, p.small, span.small, .text-small { small, p.small, span.small, .text-small {
font-size: 0.8em; font-size: 0.8em;
color: lighten($text-dark, 20%); color: lighten($text-dark, 20%);
small, p.small, span.small, .text-small {
font-size: 1em;
}
} }
sup, .superscript { sup, .superscript {

View File

@ -16,7 +16,7 @@ return [
'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.', 'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
'app_name_header' => 'Anwendungsname im Header anzeigen?', 'app_name_header' => 'Anwendungsname im Header anzeigen?',
'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?', 'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?',
'app_secure_images' => 'Erh&oml;hte Sicherheit f&uuml;r Bilduploads aktivieren?', 'app_secure_images' => 'Erh&ouml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.', 'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
'app_editor' => 'Seiteneditor', 'app_editor' => 'Seiteneditor',
'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.', 'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',

View File

@ -14,7 +14,49 @@ return [
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
/** /**
* Email Confirmation Text * Login & Register
*/
'sign_up' => 'Sign up',
'log_in' => 'Log in',
'logout' => 'Logout',
'name' => 'Name',
'username' => 'Username',
'email' => 'Email',
'password' => 'Password',
'password_confirm' => 'Confirm Password',
'password_hint' => 'Must be over 5 characters',
'forgot_password' => 'Forgot Password?',
'remember_me' => 'Remember Me',
'ldap_email_hint' => 'Please enter an email to use for this account.',
'create_account' => 'Create Account',
'social_login' => 'Social Login',
'social_registration' => 'Social Registration',
'social_registration_text' => 'Register and sign in using another service.',
'register_thanks' => 'Thanks for registering!',
'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',
'registrations_disabled' => 'Registrations are currently disabled',
'registration_email_domain_invalid' => 'That email domain does not have access to this application',
'register_success' => 'Thanks for signing up! You are now registered and signed in.',
/**
* Password Reset
*/
'reset_password' => 'Reset Password',
'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',
'reset_password_send_button' => 'Send Reset Link',
'reset_password_sent_success' => 'A password reset link has been sent to :email.',
'reset_password_success' => 'Your password has been successfully reset.',
'email_reset_subject' => 'Reset your :appName password',
'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',
'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',
/**
* Email Confirmation
*/ */
'email_confirm_subject' => 'Confirm your email on :appName', 'email_confirm_subject' => 'Confirm your email on :appName',
'email_confirm_greeting' => 'Thanks for joining :appName!', 'email_confirm_greeting' => 'Thanks for joining :appName!',
@ -23,4 +65,10 @@ return [
'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.', 'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
'email_confirm_success' => 'Your email has been confirmed!', 'email_confirm_success' => 'Your email has been confirmed!',
'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.', 'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
'email_not_confirmed' => 'Email Address Not Confirmed',
'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
]; ];

View File

@ -0,0 +1,58 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Cancel',
'confirm' => 'Confirm',
'back' => 'Back',
'save' => 'Save',
'continue' => 'Continue',
'select' => 'Select',
/**
* Form Labels
*/
'name' => 'Name',
'description' => 'Description',
'role' => 'Role',
/**
* Actions
*/
'actions' => 'Actions',
'view' => 'View',
'create' => 'Create',
'update' => 'Update',
'edit' => 'Edit',
'sort' => 'Sort',
'move' => 'Move',
'delete' => 'Delete',
'search' => 'Search',
'search_clear' => 'Clear Search',
'reset' => 'Reset',
'remove' => 'Remove',
/**
* Misc
*/
'deleted_user' => 'Deleted User',
'no_activity' => 'No activity to show',
'no_items' => 'No items available',
'back_to_top' => 'Back to top',
'toggle_details' => 'Toggle Details',
/**
* Header
*/
'view_profile' => 'View Profile',
'edit_profile' => 'Edit Profile',
/**
* Email Content
*/
'email_action_help' => 'If youre having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
'email_rights' => 'All rights reserved',
];

View File

@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Image Select',
'image_all' => 'All',
'image_all_title' => 'View all images',
'image_book_title' => 'View images uploaded to this book',
'image_page_title' => 'View images uploaded to this page',
'image_search_hint' => 'Search by image name',
'image_uploaded' => 'Uploaded :uploadedDate',
'image_load_more' => 'Load More',
'image_image_name' => 'Image Name',
'image_delete_confirm' => 'This image is used in the pages below, Click delete again to confirm you want to delete this image.',
'image_select_image' => 'Select Image',
'image_dropzone' => 'Drop images or click here to upload',
'images_deleted' => 'Images Deleted',
'image_preview' => 'Image Preview',
'image_upload_success' => 'Image uploaded successfully',
'image_update_success' => 'Image details successfully updated',
'image_delete_success' => 'Image successfully deleted'
];

View File

@ -0,0 +1,225 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'Recently Created',
'recently_created_pages' => 'Recently Created Pages',
'recently_updated_pages' => 'Recently Updated Pages',
'recently_created_chapters' => 'Recently Created Chapters',
'recently_created_books' => 'Recently Created Books',
'recently_update' => 'Recently Updated',
'recently_viewed' => 'Recently Viewed',
'recent_activity' => 'Recent Activity',
'create_now' => 'Create one now',
'revisions' => 'Revisions',
'meta_created' => 'Created :timeLength',
'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user',
'x_pages' => ':count Pages',
'entity_select' => 'Entity Select',
'images' => 'Images',
'my_recent_drafts' => 'My Recent Drafts',
'my_recently_viewed' => 'My Recently Viewed',
'no_pages_viewed' => 'You have not viewed any pages',
'no_pages_recently_created' => 'No pages have been recently created',
'no_pages_recently_updated' => 'No pages have been recently updated',
/**
* Permissions and restrictions
*/
'permissions' => 'Permissions',
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
'permissions_enable' => 'Enable Custom Permissions',
'permissions_save' => 'Save Permissions',
/**
* Search
*/
'search_results' => 'Search Results',
'search_results_page' => 'Page Search Results',
'search_results_chapter' => 'Chapter Search Results',
'search_results_book' => 'Book Search Results',
'search_clear' => 'Clear Search',
'search_view_pages' => 'View all matches pages',
'search_view_chapters' => 'View all matches chapters',
'search_view_books' => 'View all matches books',
'search_no_pages' => 'No pages matched this search',
'search_for_term' => 'Search for :term',
'search_page_for_term' => 'Page search for :term',
'search_chapter_for_term' => 'Chapter search for :term',
'search_book_for_term' => 'Books search for :term',
/**
* Books
*/
'book' => 'Book',
'books' => 'Books',
'books_empty' => 'No books have been created',
'books_popular' => 'Popular Books',
'books_recent' => 'Recent Books',
'books_popular_empty' => 'The most popular books will appear here.',
'books_create' => 'Create New Book',
'books_delete' => 'Delete Book',
'books_delete_named' => 'Delete Book :bookName',
'books_delete_explain' => 'This will delete the book with the name \':bookName\', All pages and chapters will be removed.',
'books_delete_confirmation' => 'Are you sure you want to delete this book?',
'books_edit' => 'Edit Book',
'books_edit_named' => 'Edit Book :bookName',
'books_form_book_name' => 'Book Name',
'books_save' => 'Save Book',
'books_permissions' => 'Book Permissions',
'books_permissions_updated' => 'Book Permissions Updated',
'books_empty_contents' => 'No pages or chapters have been created for this book.',
'books_empty_create_page' => 'Create a new page',
'books_empty_or' => 'or',
'books_empty_sort_current_book' => 'Sort the current book',
'books_empty_add_chapter' => 'Add a chapter',
'books_permissions_active' => 'Book Permissions Active',
'books_search_this' => 'Search this book',
'books_navigation' => 'Book Navigation',
'books_sort' => 'Sort Book Contents',
'books_sort_named' => 'Sort Book :bookName',
'books_sort_show_other' => 'Show Other Books',
'books_sort_save' => 'Save New Order',
/**
* Chapters
*/
'chapter' => 'Chapter',
'chapters_popular' => 'Popular Chapters',
'chapters_new' => 'New Chapter',
'chapters_create' => 'Create New Chapter',
'chapters_delete' => 'Delete Chapter',
'chapters_delete_named' => 'Delete Chapter :chapterName',
'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\', All pages will be removed
and added directly to the parent book.',
'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',
'chapters_edit' => 'Edit Chapter',
'chapters_edit_named' => 'Edit Chapter :chapterName',
'chapters_save' => 'Save Chapter',
'chapters_move' => 'Move Chapter',
'chapters_move_named' => 'Move Chapter :chapterName',
'chapter_move_success' => 'Chapter moved to :bookName',
'chapters_permissions' => 'Chapter Permissions',
'chapters_empty' => 'No pages are currently in this chapter.',
'chapters_permissions_active' => 'Chapter Permissions Active',
'chapters_permissions_success' => 'Chapter Permissions Updated',
/**
* Pages
*/
'page' => 'Page',
'pages' => 'Pages',
'pages_popular' => 'Popular Pages',
'pages_new' => 'New Page',
'pages_attachments' => 'Attachments',
'pages_navigation' => 'Page Navigation',
'pages_delete' => 'Delete Page',
'pages_delete_named' => 'Delete Page :pageName',
'pages_delete_draft_named' => 'Delete Draft Page :pageName',
'pages_delete_draft' => 'Delete Draft Page',
'pages_delete_success' => 'Page deleted',
'pages_delete_draft_success' => 'Draft page deleted',
'pages_delete_confirm' => 'Are you sure you want to delete this page?',
'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
'pages_editing_named' => 'Editing Page :pageName',
'pages_edit_toggle_header' => 'Toggle header',
'pages_edit_save_draft' => 'Save Draft',
'pages_edit_draft' => 'Edit Page Draft',
'pages_editing_draft' => 'Editing Draft',
'pages_editing_page' => 'Editing Page',
'pages_edit_draft_save_at' => 'Draft saved at ',
'pages_edit_delete_draft' => 'Delete Draft',
'pages_edit_discard_draft' => 'Discard Draft',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
'pages_save' => 'Save Page',
'pages_title' => 'Page Title',
'pages_name' => 'Page Name',
'pages_md_editor' => 'Editor',
'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Insert Image',
'pages_md_insert_link' => 'Insert Entity Link',
'pages_not_in_chapter' => 'Page is not in a chapter',
'pages_move' => 'Move Page',
'pages_move_success' => 'Page moved to ":parentName"',
'pages_permissions' => 'Page Permissions',
'pages_permissions_success' => 'Page permissions updated',
'pages_revisions' => 'Page Revisions',
'pages_revisions_named' => 'Page Revisions for :pageName',
'pages_revision_named' => 'Page Revision for :pageName',
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restore',
'pages_revisions_none' => 'This page has no revisions',
'pages_export' => 'Export',
'pages_export_html' => 'Contained Web File',
'pages_export_pdf' => 'PDF File',
'pages_export_text' => 'Plain Text File',
'pages_copy_link' => 'Copy Link',
'pages_permissions_active' => 'Page Permissions Active',
'pages_initial_revision' => 'Initial publish',
'pages_initial_name' => 'New Page',
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
'start_b' => ':userName has started editing this page',
'time_a' => 'since the pages was last updated',
'time_b' => 'in the last :minCount minutes',
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
/**
* Editor sidebar
*/
'page_tags' => 'Page Tags',
'tag' => 'Tag',
'tags' => '',
'tag_value' => 'Tag Value (Optional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
'tags_add' => 'Add another tag',
'attachments' => 'Attachments',
'attachments_explain' => 'Upload some files or attach some link to display on your page. These are visible in the page sidebar.',
'attachments_explain_instant_save' => 'Changes here are saved instantly.',
'attachments_items' => 'Attached Items',
'attachments_upload' => 'Upload File',
'attachments_link' => 'Attach Link',
'attachments_set_link' => 'Set Link',
'attachments_delete_confirm' => 'Click delete again to confirm you want to delete this attachment.',
'attachments_dropzone' => 'Drop files or click here to attach a file',
'attachments_no_files' => 'No files have been uploaded',
'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',
'attachments_link_name' => 'Link Name',
'attachment_link' => 'Attachment link',
'attachments_link_url' => 'Link to file',
'attachments_link_url_hint' => 'Url of site or file',
'attach' => 'Attach',
'attachments_edit_file' => 'Edit File',
'attachments_edit_file_name' => 'File Name',
'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
'attachments_order_updated' => 'Attachment order updated',
'attachments_updated_success' => 'Attachment details updated',
'attachments_deleted' => 'Attachment deleted',
'attachments_file_uploaded' => 'File successfully uploaded',
'attachments_file_updated' => 'File successfully updated',
'attachments_link_attached' => 'Link successfully attached to page',
/**
* Profile View
*/
'profile_user_for_x' => 'User for :time',
'profile_created_content' => 'Created Content',
'profile_not_created_pages' => ':userName has not created any pages',
'profile_not_created_chapters' => ':userName has not created any chapters',
'profile_not_created_books' => ':userName has not created any books',
];

View File

@ -6,7 +6,65 @@ return [
* Error text strings. * Error text strings.
*/ */
// Pages // Permissions
'permission' => 'You do not have permission to access the requested page.', 'permission' => 'You do not have permission to access the requested page.',
'permissionJson' => 'You do not have permission to perform the requested action.' 'permissionJson' => 'You do not have permission to perform the requested action.',
// Auth
'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',
'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',
'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',
'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',
'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
'social_no_action_defined' => 'No action defined',
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',
'social_account_existing' => 'This :socialAccount is already attached to your profile.',
'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',
'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'cannot_get_image_from_url' => 'Cannot get image from :url',
'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'image_upload_error' => 'An error occurred uploading the image',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
// Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
// Entities
'entity_not_found' => 'Entity not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',
'selected_book_not_found' => 'The selected book was not found',
'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',
'guests_cannot_save_drafts' => 'Guests cannot save drafts',
// Users
'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
'users_cannot_delete_guest' => 'You cannot delete the guest user',
// Roles
'role_cannot_be_edited' => 'This role cannot be edited',
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
// Error pages
'404_page_not_found' => 'Page Not Found',
'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
'return_home' => 'Return to home',
'error_occurred' => 'An Error Occurred',
'app_down' => ':appName is down right now',
'back_soon' => 'It will be back up soon.',
]; ];

View File

@ -10,6 +10,11 @@ return [
'settings' => 'Settings', 'settings' => 'Settings',
'settings_save' => 'Save Settings', 'settings_save' => 'Save Settings',
'settings_save_success' => 'Settings saved',
/**
* App settings
*/
'app_settings' => 'App Settings', 'app_settings' => 'App Settings',
'app_name' => 'Application name', 'app_name' => 'Application name',
@ -27,6 +32,10 @@ return [
'app_primary_color' => 'Application primary color', 'app_primary_color' => 'Application primary color',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.', 'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
/**
* Registration settings
*/
'reg_settings' => 'Registration Settings', 'reg_settings' => 'Registration Settings',
'reg_allow' => 'Allow registration?', 'reg_allow' => 'Allow registration?',
'reg_default_role' => 'Default user role after registration', 'reg_default_role' => 'Default user role after registration',
@ -36,4 +45,96 @@ return [
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.', 'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set', 'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
/**
* Role settings
*/
'roles' => 'Roles',
'role_user_roles' => 'User Roles',
'role_create' => 'Create New Role',
'role_create_success' => 'Role successfully created',
'role_delete' => 'Delete Role',
'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.',
'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
'role_delete_no_migration' => "Don't migrate users",
'role_delete_sure' => 'Are you sure you want to delete this role?',
'role_delete_success' => 'Role successfully deleted',
'role_edit' => 'Edit Role',
'role_details' => 'Role Details',
'role_name' => 'Role Name',
'role_desc' => 'Short Description of Role',
'role_system' => 'System Permissions',
'role_manage_users' => 'Manage users',
'role_manage_roles' => 'Manage roles & role permissions',
'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',
'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
'role_manage_settings' => 'Manage app settings',
'role_asset' => 'Asset Permissions',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
'role_all' => 'All',
'role_own' => 'Own',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Save Role',
'role_update_success' => 'Role successfully updated',
'role_users' => 'Users in this role',
'role_users_none' => 'No users are currently assigned to this role',
/**
* Users
*/
'users' => 'Users',
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
'users_role' => 'User Roles',
'users_external_auth_id' => 'External Authentication ID',
'users_password_warning' => 'Only fill the below if you would like to change your password:',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_delete' => 'Delete User',
'users_delete_named' => 'Delete ser :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
'users_delete_confirm' => 'Are you sure you want to delete this user?',
'users_delete_success' => 'Users successfully removed',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
'users_edit_success' => 'User successfully updated',
'users_avatar' => 'User Avatar',
'users_avatar_desc' => 'This image should be approx 256px square.',
'users_social_accounts' => 'Social Accounts',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.',
'users_social_connect' => 'Connect Account',
'users_social_disconnect' => 'Disconnect Account',
'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
]; ];

View File

@ -87,8 +87,8 @@ return [
*/ */
'custom' => [ 'custom' => [
'attribute-name' => [ 'password-confirm' => [
'rule-name' => 'custom-message', 'required_with' => 'Password confirmation required',
], ],
], ],

View File

@ -1,19 +1,19 @@
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">{{ trans('auth.username') }}</label>
@include('form/text', ['name' => 'username', 'tabindex' => 1]) @include('form/text', ['name' => 'username', 'tabindex' => 1])
</div> </div>
@if(session('request-email', false) === true) @if(session('request-email', false) === true)
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1]) @include('form/text', ['name' => 'email', 'tabindex' => 1])
<span class="text-neg"> <span class="text-neg">
Please enter an email to use for this account. {{ trans('auth.ldap_email_hint') }}
</span> </span>
</div> </div>
@endif @endif
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2]) @include('form/password', ['name' => 'password', 'tabindex' => 2])
</div> </div>

View File

@ -1,10 +1,10 @@
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email', 'tabindex' => 1]) @include('form/text', ['name' => 'email', 'tabindex' => 1])
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password', 'tabindex' => 2]) @include('form/password', ['name' => 'password', 'tabindex' => 2])
<span class="block small"><a href="{{ baseUrl('/password/email') }}">Forgot Password?</a></span> <span class="block small"><a href="{{ baseUrl('/password/email') }}">{{ trans('auth.forgot_password') }}</a></span>
</div> </div>

View File

@ -2,7 +2,7 @@
@section('header-buttons') @section('header-buttons')
@if(setting('registration-enabled', false)) @if(setting('registration-enabled', false))
<a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a> <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>{{ trans('auth.sign_up') }}</a>
@endif @endif
@stop @stop
@ -10,7 +10,7 @@
<div class="text-center"> <div class="text-center">
<div class="center-box"> <div class="center-box">
<h1>Log In</h1> <h1>{{ title_case(trans('auth.log_in')) }}</h1>
<form action="{{ baseUrl("/login") }}" method="POST" id="login-form"> <form action="{{ baseUrl("/login") }}" method="POST" id="login-form">
{!! csrf_field() !!} {!! csrf_field() !!}
@ -19,25 +19,25 @@
@include('auth/forms/login/' . $authMethod) @include('auth/forms/login/' . $authMethod)
<div class="form-group"> <div class="form-group">
<label for="remember" class="inline">Remember Me</label> <label for="remember" class="inline">{{ trans('auth.remember_me') }}</label>
<input type="checkbox" id="remember" name="remember" class="toggle-switch-checkbox"> <input type="checkbox" id="remember" name="remember" class="toggle-switch-checkbox">
<label for="remember" class="toggle-switch"></label> <label for="remember" class="toggle-switch"></label>
</div> </div>
<div class="from-group"> <div class="from-group">
<button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> Sign In</button> <button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> {{ title_case(trans('auth.log_in')) }}</button>
</div> </div>
</form> </form>
@if(count($socialDrivers) > 0) @if(count($socialDrivers) > 0)
<hr class="margin-top"> <hr class="margin-top">
<h3 class="text-muted">Social Login</h3> <h3 class="text-muted">{{ trans('auth.social_login') }}</h3>
@if(isset($socialDrivers['google'])) @if(isset($socialDrivers['google']))
<a href="{{ baseUrl("/login/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a> <a id="social-login-google" href="{{ baseUrl("/login/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
@endif @endif
@if(isset($socialDrivers['github'])) @if(isset($socialDrivers['github']))
<a href="{{ baseUrl("/login/service/github") }}" style="color:#444;"><i class="zmdi zmdi-github zmdi-hc-4x"></i></a> <a id="social-login-github" href="{{ baseUrl("/login/service/github") }}" style="color:#444;"><i class="zmdi zmdi-github zmdi-hc-4x"></i></a>
@endif @endif
@endif @endif
</div> </div>

View File

@ -1,9 +1,9 @@
@extends('public') @extends('public')
@section('header-buttons') @section('header-buttons')
<a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a> <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
@if(setting('registration-enabled')) @if(setting('registration-enabled'))
<a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a> <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>{{ trans('auth.sign_up') }}</a>
@endif @endif
@stop @stop
@ -12,20 +12,20 @@
<div class="text-center"> <div class="text-center">
<div class="center-box text-left"> <div class="center-box text-left">
<h1>Reset Password</h1> <h1>{{ trans('auth.reset_password') }}</h1>
<p class="muted small">Enter your email below and you will be sent an email with a password reset link.</p> <p class="muted small">{{ trans('auth.reset_password_send_instructions') }}</p>
<form action="{{ baseUrl("/password/email") }}" method="POST"> <form action="{{ baseUrl("/password/email") }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email']) @include('form/text', ['name' => 'email'])
</div> </div>
<div class="from-group"> <div class="from-group">
<button class="button block pos">Send Reset Link</button> <button class="button block pos">{{ trans('auth.reset_password_send_button') }}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,9 +1,9 @@
@extends('public') @extends('public')
@section('header-buttons') @section('header-buttons')
<a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a> <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
@if(setting('registration-enabled')) @if(setting('registration-enabled'))
<a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>Sign up</a> <a href="{{ baseUrl("/register") }}"><i class="zmdi zmdi-account-add"></i>{{ trans('auth.sign_up') }}</a>
@endif @endif
@stop @stop
@ -14,29 +14,29 @@
<div class="text-center"> <div class="text-center">
<div class="center-box text-left"> <div class="center-box text-left">
<h1>Reset Password</h1> <h1>{{ trans('auth.reset_password') }}</h1>
<form action="{{ baseUrl("/password/reset") }}" method="POST"> <form action="{{ baseUrl("/password/reset") }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="token" value="{{ $token }}"> <input type="hidden" name="token" value="{{ $token }}">
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email']) @include('form/text', ['name' => 'email'])
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password']) @include('form/password', ['name' => 'password'])
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password_confirmation">Confirm Password</label> <label for="password_confirmation">{{ trans('auth.password_confirm') }}</label>
@include('form/password', ['name' => 'password_confirmation']) @include('form/password', ['name' => 'password_confirmation'])
</div> </div>
<div class="from-group"> <div class="from-group">
<button class="button block pos">Reset Password</button> <button class="button block pos">{{ trans('auth.reset_password') }}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -2,7 +2,7 @@
@section('header-buttons') @section('header-buttons')
@if(!$signedIn) @if(!$signedIn)
<a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a> <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
@endif @endif
@stop @stop
@ -10,10 +10,9 @@
<div class="text-center"> <div class="text-center">
<div class="center-box"> <div class="center-box">
<h2>Thanks for registering!</h2> <h2>{{ trans('auth.register_thanks') }}</h2>
<p>Please check your email and click the confirmation button to access {{ setting('app-name', 'BookStack') }}.</p> <p>{{ trans('auth.register_confirm', ['appName' => setting('app-name')]) }}</p>
</div> </div>
</div> </div>
@stop @stop

View File

@ -1,42 +1,42 @@
@extends('public') @extends('public')
@section('header-buttons') @section('header-buttons')
<a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>Sign in</a> <a href="{{ baseUrl("/login") }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
@stop @stop
@section('content') @section('content')
<div class="text-center"> <div class="text-center">
<div class="center-box"> <div class="center-box">
<h1>Sign Up</h1> <h1>{{ title_case(trans('auth.sign_up')) }}</h1>
<form action="{{ baseUrl("/register") }}" method="POST"> <form action="{{ baseUrl("/register") }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<div class="form-group"> <div class="form-group">
<label for="email">Name</label> <label for="email">{{ trans('auth.name') }}</label>
@include('form/text', ['name' => 'name']) @include('form/text', ['name' => 'name'])
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">{{ trans('auth.email') }}</label>
@include('form/text', ['name' => 'email']) @include('form/text', ['name' => 'email'])
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">{{ trans('auth.password') }}</label>
@include('form/password', ['name' => 'password', 'placeholder' => 'Must be over 5 characters']) @include('form/password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
</div> </div>
<div class="from-group"> <div class="from-group">
<button class="button block pos">Create Account</button> <button class="button block pos">{{ trans('auth.create_account') }}</button>
</div> </div>
</form> </form>
@if(count($socialDrivers) > 0) @if(count($socialDrivers) > 0)
<hr class="margin-top"> <hr class="margin-top">
<h3 class="text-muted">Social Registration</h3> <h3 class="text-muted">{{ trans('auth.social_registration') }}</h3>
<p class="text-small">Register and sign in using another service.</p> <p class="text-small">{{ trans('auth.social_registration_text') }}</p>
@if(isset($socialDrivers['google'])) @if(isset($socialDrivers['google']))
<a href="{{ baseUrl("/register/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a> <a href="{{ baseUrl("/register/service/google") }}" style="color: #DC4E41;"><i class="zmdi zmdi-google-plus-box zmdi-hc-4x"></i></a>
@endif @endif

View File

@ -4,16 +4,16 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
<h2>Email Address not confirmed</h2> <h2>{{ trans('auth.email_not_confirmed') }}</h2>
<p class="text-muted">Your email address has not yet been confirmed. <br> <p class="text-muted">{{ trans('auth.email_not_confirmed_text') }}<br>
Please click the link in the email that was sent shortly after you registered. <br> {{ trans('auth.email_not_confirmed_click_link') }} <br>
If you cannot find the email you can re-send the confirmation email by submitting the form below. {{ trans('auth.email_not_confirmed_resend') }}
</p> </p>
<hr> <hr>
<form action="{{ baseUrl("/register/confirm/resend") }}" method="POST"> <form action="{{ baseUrl("/register/confirm/resend") }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<div class="form-group"> <div class="form-group">
<label for="email">Email Address</label> <label for="email">{{ trans('auth.email') }}</label>
@if(auth()->check()) @if(auth()->check())
@include('form/text', ['name' => 'email', 'model' => auth()->user()]) @include('form/text', ['name' => 'email', 'model' => auth()->user()])
@else @else
@ -21,7 +21,7 @@
@endif @endif
</div> </div>
<div class="form-group"> <div class="form-group">
<button type="submit" class="button pos">Resend Confirmation Email</button> <button type="submit" class="button pos">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -17,6 +17,7 @@
<!-- Scripts --> <!-- Scripts -->
<script src="{{ baseUrl('/libs/jquery/jquery.min.js?version=2.1.4') }}"></script> <script src="{{ baseUrl('/libs/jquery/jquery.min.js?version=2.1.4') }}"></script>
<script src="{{ baseUrl('/libs/jquery/jquery-ui.min.js?version=1.11.4') }}"></script> <script src="{{ baseUrl('/libs/jquery/jquery-ui.min.js?version=1.11.4') }}"></script>
<script src="{{ baseUrl('/translations.js') }}"></script>
@yield('head') @yield('head')
@ -53,32 +54,16 @@
<div class="col-lg-4 col-sm-5"> <div class="col-lg-4 col-sm-5">
<div class="float right"> <div class="float right">
<div class="links text-center"> <div class="links text-center">
<a href="{{ baseUrl('/books') }}"><i class="zmdi zmdi-book"></i>Books</a> <a href="{{ baseUrl('/books') }}"><i class="zmdi zmdi-book"></i>{{ trans('entities.books') }}</a>
@if(isset($currentUser) && userCan('settings-manage')) @if(isset($currentUser) && userCan('settings-manage'))
<a href="{{ baseUrl('/settings') }}"><i class="zmdi zmdi-settings"></i>Settings</a> <a href="{{ baseUrl('/settings') }}"><i class="zmdi zmdi-settings"></i>{{ trans('settings.settings') }}</a>
@endif @endif
@if(!isset($signedIn) || !$signedIn) @if(!isset($signedIn) || !$signedIn)
<a href="{{ baseUrl('/login') }}"><i class="zmdi zmdi-sign-in"></i>Sign In</a> <a href="{{ baseUrl('/login') }}"><i class="zmdi zmdi-sign-in"></i>{{ trans('auth.log_in') }}</a>
@endif @endif
</div> </div>
@if(isset($signedIn) && $signedIn) @if(isset($signedIn) && $signedIn)
<div class="dropdown-container" dropdown> @include('partials._header-dropdown', ['currentUser' => $currentUser])
<span class="user-name" dropdown-toggle>
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
<span class="name" ng-non-bindable>{{ $currentUser->getShortName(9) }}</span> <i class="zmdi zmdi-caret-down"></i>
</span>
<ul>
<li>
<a href="{{ baseUrl("/user/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-account zmdi-hc-fw zmdi-hc-lg"></i>View Profile</a>
</li>
<li>
<a href="{{ baseUrl("/settings/users/{$currentUser->id}") }}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-fw zmdi-hc-lg"></i>Edit Profile</a>
</li>
<li>
<a href="{{ baseUrl('/logout') }}" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-fw zmdi-hc-lg"></i>Logout</a>
</li>
</ul>
</div>
@endif @endif
</div> </div>
@ -93,7 +78,7 @@
<div id="back-to-top"> <div id="back-to-top">
<div class="inner"> <div class="inner">
<i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span> <i class="zmdi zmdi-chevron-up"></i> <span>{{ trans('common.back_to_top') }}</span>
</div> </div>
</div> </div>
@yield('bottom') @yield('bottom')

View File

@ -0,0 +1,3 @@
<div class="breadcrumbs">
<a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>

View File

@ -3,7 +3,7 @@
@section('content') @section('content')
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Create New Book</h1> <h1>{{ trans('entities.books_create') }}</h1>
<form action="{{ baseUrl("/books") }}" method="POST"> <form action="{{ baseUrl("/books") }}" method="POST">
@include('books/form') @include('books/form')
</form> </form>

View File

@ -2,16 +2,26 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
</div>
</div>
</div>
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Delete Book</h1> <h1>{{ trans('entities.books_delete') }}</h1>
<p>This will delete the book with the name '{{$book->name}}', All pages and chapters will be removed.</p> <p>{{ trans('entities.books_delete_explain', ['bookName' => $book->name]) }}</p>
<p class="text-neg">Are you sure you want to delete this book?</p> <p class="text-neg">{{ trans('entities.books_delete_confirmation') }}</p>
<form action="{{$book->getUrl()}}" method="POST"> <form action="{{$book->getUrl()}}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE"> <input type="hidden" name="_method" value="DELETE">
<a href="{{$book->getUrl()}}" class="button">Cancel</a> <a href="{{$book->getUrl()}}" class="button">{{ trans('common.cancel') }}</a>
<button type="submit" class="button neg">Confirm</button> <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
</form> </form>
</div> </div>

View File

@ -2,8 +2,18 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
</div>
</div>
</div>
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Edit Book</h1> <h1>{{ trans('entities.books_edit') }}</h1>
<form action="{{ $book->getUrl() }}" method="POST"> <form action="{{ $book->getUrl() }}" method="POST">
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
@include('books/form', ['model' => $book]) @include('books/form', ['model' => $book])

View File

@ -1,16 +1,16 @@
{{ csrf_field() }} {{ csrf_field() }}
<div class="form-group title-input"> <div class="form-group title-input">
<label for="name">Book Name</label> <label for="name">{{ trans('common.name') }}</label>
@include('form/text', ['name' => 'name']) @include('form/text', ['name' => 'name'])
</div> </div>
<div class="form-group description-input"> <div class="form-group description-input">
<label for="description">Description</label> <label for="description">{{ trans('common.description') }}</label>
@include('form/textarea', ['name' => 'description']) @include('form/textarea', ['name' => 'description'])
</div> </div>
<div class="form-group"> <div class="form-group">
<a href="{{ back()->getTargetUrl() }}" class="button muted">Cancel</a> <a href="{{ back()->getTargetUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">Save Book</button> <button type="submit" class="button pos">{{ trans('entities.books_save') }}</button>
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="col-xs-11 faded"> <div class="col-xs-11 faded">
<div class="action-buttons"> <div class="action-buttons">
@if($currentUser->can('book-create-all')) @if($currentUser->can('book-create-all'))
<a href="{{ baseUrl("/books/create") }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>Add new book</a> <a href="{{ baseUrl("/books/create") }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.books_create') }}</a>
@endif @endif
</div> </div>
</div> </div>
@ -21,7 +21,7 @@
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<div class="row"> <div class="row">
<div class="col-sm-7"> <div class="col-sm-7">
<h1>Books</h1> <h1>{{ trans('entities.books') }}</h1>
@if(count($books) > 0) @if(count($books) > 0)
@foreach($books as $book) @foreach($books as $book)
@include('books/list-item', ['book' => $book]) @include('books/list-item', ['book' => $book])
@ -29,27 +29,27 @@
@endforeach @endforeach
{!! $books->render() !!} {!! $books->render() !!}
@else @else
<p class="text-muted">No books have been created.</p> <p class="text-muted">{{ trans('entities.books_empty') }}</p>
@if(userCan('books-create-all')) @if(userCan('books-create-all'))
<a href="{{ baseUrl("/books/create") }}" class="text-pos"><i class="zmdi zmdi-edit"></i>Create one now</a> <a href="{{ baseUrl("/books/create") }}" class="text-pos"><i class="zmdi zmdi-edit"></i>{{ trans('entities.create_one_now') }}</a>
@endif @endif
@endif @endif
</div> </div>
<div class="col-sm-4 col-sm-offset-1"> <div class="col-sm-4 col-sm-offset-1">
<div id="recents"> <div id="recents">
@if($recents) @if($recents)
<div class="margin-top large">&nbsp;</div> <div class="margin-top">&nbsp;</div>
<h3>Recently Viewed</h3> <h3>{{ trans('entities.recently_viewed') }}</h3>
@include('partials/entity-list', ['entities' => $recents]) @include('partials/entity-list', ['entities' => $recents])
@endif @endif
</div> </div>
<div class="margin-top large">&nbsp;</div> <div class="margin-top large">&nbsp;</div>
<div id="popular"> <div id="popular">
<h3>Popular Books</h3> <h3>{{ trans('entities.books_popular') }}</h3>
@if(count($popular) > 0) @if(count($popular) > 0)
@include('partials/entity-list', ['entities' => $popular]) @include('partials/entity-list', ['entities' => $popular])
@else @else
<p class="text-muted">The most popular books will appear here.</p> <p class="text-muted">{{ trans('entities.books_popular_empty') }}</p>
@endif @endif
</div> </div>
</div> </div>

View File

@ -6,9 +6,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-12 faded"> <div class="col-sm-12 faded">
<div class="breadcrumbs"> @include('books._breadcrumbs', ['book' => $book])
<a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -16,7 +14,7 @@
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<h1>Book Permissions</h1> <h1>{{ trans('entities.books_permissions') }}</h1>
@include('form/restriction-form', ['model' => $book]) @include('form/restriction-form', ['model' => $book])
</div> </div>

View File

@ -5,29 +5,32 @@
<div class="faded-small toolbar"> <div class="faded-small toolbar">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-6 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
<div class="col-md-6">
<div class="action-buttons faded"> <div class="action-buttons faded">
@if(userCan('page-create', $book)) @if(userCan('page-create', $book))
<a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Page</a> <a href="{{ $book->getUrl('/page/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
@endif @endif
@if(userCan('chapter-create', $book)) @if(userCan('chapter-create', $book))
<a href="{{ $book->getUrl('/chapter/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i> New Chapter</a> <a href="{{ $book->getUrl('/chapter/create') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.chapters_new') }}</a>
@endif @endif
@if(userCan('book-update', $book)) @if(userCan('book-update', $book))
<a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> <a href="{{$book->getEditUrl()}}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>{{ trans('common.edit') }}</a>
@endif @endif
@if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book)) @if(userCan('book-update', $book) || userCan('restrictions-manage', $book) || userCan('book-delete', $book))
<div dropdown class="dropdown-container"> <div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a> <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
<ul> <ul>
@if(userCan('book-update', $book)) @if(userCan('book-update', $book))
<li><a href="{{ $book->getUrl('/sort') }}" class="text-primary"><i class="zmdi zmdi-sort"></i>Sort</a></li> <li><a href="{{ $book->getUrl('/sort') }}" class="text-primary"><i class="zmdi zmdi-sort"></i>{{ trans('common.sort') }}</a></li>
@endif @endif
@if(userCan('restrictions-manage', $book)) @if(userCan('restrictions-manage', $book))
<li><a href="{{ $book->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li> <li><a href="{{ $book->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.permissions') }}</a></li>
@endif @endif
@if(userCan('book-delete', $book)) @if(userCan('book-delete', $book))
<li><a href="{{ $book->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li> <li><a href="{{ $book->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
@endif @endif
</ul> </ul>
</div> </div>
@ -59,23 +62,19 @@
<hr> <hr>
@endforeach @endforeach
@else @else
<p class="text-muted">No pages or chapters have been created for this book.</p> <p class="text-muted">{{ trans('entities.books_empty_contents') }}</p>
<p> <p>
<a href="{{ $book->getUrl('/page/create') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a> <a href="{{ $book->getUrl('/page/create') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
&nbsp;&nbsp;<em class="text-muted">-or-</em>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
<a href="{{ $book->getUrl('/chapter/create') }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>Add a chapter</a> <a href="{{ $book->getUrl('/chapter/create') }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.books_empty_add_chapter') }}</a>
</p> </p>
<hr> <hr>
@endif @endif
<p class="text-muted small"> @include('partials.entity-meta', ['entity' => $book])
Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by <a href="{{ $book->createdBy->getProfileUrl() }}">{{$book->createdBy->name}}</a> @endif
<br>
Last Updated {{$book->updated_at->diffForHumans()}} @if($book->updatedBy) by <a href="{{ $book->updatedBy->getProfileUrl() }}">{{$book->updatedBy->name}}</a> @endif
</p>
</div> </div>
</div> </div>
<div class="search-results" ng-cloak ng-show="searching"> <div class="search-results" ng-cloak ng-show="searching">
<h3 class="text-muted">Search Results <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>Clear Search</a></h3> <h3 class="text-muted">{{ trans('entities.search_results') }} <a ng-if="searching" ng-click="clearSearch()" class="text-small"><i class="zmdi zmdi-close"></i>{{ trans('entities.search_clear') }}</a></h3>
<div ng-if="!searchResults"> <div ng-if="!searchResults">
@include('partials/loading-icon') @include('partials/loading-icon')
</div> </div>
@ -90,21 +89,21 @@
@if($book->restricted) @if($book->restricted)
<p class="text-muted"> <p class="text-muted">
@if(userCan('restrictions-manage', $book)) @if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a> <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}</a>
@else @else
<i class="zmdi zmdi-lock-outline"></i>Book Permissions Active <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}
@endif @endif
</p> </p>
@endif @endif
<div class="search-box"> <div class="search-box">
<form ng-submit="searchBook($event)"> <form ng-submit="searchBook($event)">
<input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="Search This Book"> <input ng-model="searchTerm" ng-change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button> <button type="submit"><i class="zmdi zmdi-search"></i></button>
<button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button> <button ng-if="searching" ng-click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>
</form> </form>
</div> </div>
<div class="activity anim fadeIn"> <div class="activity anim fadeIn">
<h3>Recent Activity</h3> <h3>{{ trans('entities.recent_activity') }}</h3>
@include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)]) @include('partials/activity-list', ['activity' => Activity::entityActivity($book, 20, 0)])
</div> </div>
</div> </div>

View File

@ -6,8 +6,18 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
</div>
</div>
</div>
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<h1>Sorting Pages & Chapters<span class="subheader">For {{ $book->name }}</span></h1> <h1>{{ trans('entities.books_sort') }}</h1>
<div class="row"> <div class="row">
<div class="col-md-8" id="sort-boxes"> <div class="col-md-8" id="sort-boxes">
@ -17,7 +27,7 @@
@if(count($books) > 1) @if(count($books) > 1)
<div class="col-md-4"> <div class="col-md-4">
<h3>Show Other Books</h3> <h3>{{ trans('entities.books_sort_show_other') }}</h3>
<div id="additional-books"> <div id="additional-books">
@foreach($books as $otherBook) @foreach($books as $otherBook)
@if($otherBook->id !== $book->id) @if($otherBook->id !== $book->id)
@ -37,8 +47,8 @@
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
<input type="hidden" id="sort-tree-input" name="sort-tree"> <input type="hidden" id="sort-tree-input" name="sort-tree">
<div class="list"> <div class="list">
<a href="{{ $book->getUrl() }}" class="button muted">Cancel</a> <a href="{{ $book->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
<button class="button pos" type="submit">Save Order</button> <button class="button pos" type="submit">{{ trans('entities.books_sort_save') }}</button>
</div> </div>
</form> </form>

View File

@ -0,0 +1,5 @@
<div class="breadcrumbs">
<a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span>
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div>

View File

@ -3,7 +3,7 @@
@section('content') @section('content')
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Create New Chapter</h1> <h1>{{ trans('entities.chapters_create') }}</h1>
<form action="{{ $book->getUrl('/chapter/create') }}" method="POST"> <form action="{{ $book->getUrl('/chapter/create') }}" method="POST">
@include('chapters/form') @include('chapters/form')
</form> </form>

View File

@ -2,17 +2,26 @@
@section('content') @section('content')
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-12 faded">
@include('chapters._breadcrumbs', ['chapter' => $chapter])
</div>
</div>
</div>
</div>
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Delete Chapter</h1> <h1>{{ trans('entities.chapters_delete') }}</h1>
<p>This will delete the chapter with the name '{{$chapter->name}}', All pages will be removed <p>{{ trans('entities.chapters_delete_explain', ['chapterName' => $chapter->name]) }}</p>
and added directly to the book.</p> <p class="text-neg">{{ trans('entities.chapters_delete_confirm') }}</p>
<p class="text-neg">Are you sure you want to delete this chapter?</p>
<form action="{{ $chapter->getUrl() }}" method="POST"> <form action="{{ $chapter->getUrl() }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE"> <input type="hidden" name="_method" value="DELETE">
<a href="{{ $chapter->getUrl() }}" class="button primary">Cancel</a> <a href="{{ $chapter->getUrl() }}" class="button primary">{{ trans('common.cancel') }}</a>
<button type="submit" class="button neg">Confirm</button> <button type="submit" class="button neg">{{ trans('common.confirm') }}</button>
</form> </form>
</div> </div>

View File

@ -3,7 +3,7 @@
@section('content') @section('content')
<div class="container small" ng-non-bindable> <div class="container small" ng-non-bindable>
<h1>Edit Chapter</h1> <h1>{{ trans('entities.chapters_edit') }}</h1>
<form action="{{ $chapter->getUrl() }}" method="POST"> <form action="{{ $chapter->getUrl() }}" method="POST">
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
@include('chapters/form', ['model' => $chapter]) @include('chapters/form', ['model' => $chapter])

View File

@ -2,16 +2,16 @@
{!! csrf_field() !!} {!! csrf_field() !!}
<div class="form-group title-input"> <div class="form-group title-input">
<label for="name">Chapter Name</label> <label for="name">{{ trans('common.name') }}</label>
@include('form/text', ['name' => 'name']) @include('form/text', ['name' => 'name'])
</div> </div>
<div class="form-group description-input"> <div class="form-group description-input">
<label for="description">Description</label> <label for="description">{{ trans('common.description') }}</label>
@include('form/textarea', ['name' => 'description']) @include('form/textarea', ['name' => 'description'])
</div> </div>
<div class="form-group"> <div class="form-group">
<a href="{{ back()->getTargetUrl() }}" class="button muted">Cancel</a> <a href="{{ back()->getTargetUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">Save Chapter</button> <button type="submit" class="button pos">{{ trans('entities.chapters_save') }}</button>
</div> </div>

View File

@ -17,7 +17,7 @@
@endif @endif
@if(!isset($hidePages) && count($chapter->pages) > 0) @if(!isset($hidePages) && count($chapter->pages) > 0)
<p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p> <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p>
<div class="inset-list"> <div class="inset-list">
@foreach($chapter->pages as $page) @foreach($chapter->pages as $page)
<h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5> <h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>

View File

@ -6,27 +6,23 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-12 faded"> <div class="col-sm-12 faded">
<div class="breadcrumbs"> @include('chapters._breadcrumbs', ['chapter' => $chapter])
<a href="{{ $book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
<span class="sep">&raquo;</span>
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{ $chapter->getShortName() }}</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="container"> <div class="container">
<h1>Move Chapter <small class="subheader">{{$chapter->name}}</small></h1> <h1>{{ trans('entities.chapters_move') }}</h1>
<form action="{{ $chapter->getUrl('/move') }}" method="POST"> <form action="{{ $chapter->getUrl('/move') }}" method="POST">
{!! csrf_field() !!} {!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_method" value="PUT">
@include('partials/entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book']) @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
<a href="{{ $chapter->getUrl() }}" class="button muted">Cancel</a> <a href="{{ $chapter->getUrl() }}" class="button muted">{{ trans('common.cancel') }}</a>
<button type="submit" class="button pos">Move Chapter</button> <button type="submit" class="button pos">{{ trans('entities.chapters_move') }}</button>
</form> </form>
</div> </div>

View File

@ -6,18 +6,14 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-12 faded"> <div class="col-sm-12 faded">
<div class="breadcrumbs"> @include('chapters._breadcrumbs', ['chapter' => $chapter])
<a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span>
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="container" ng-non-bindable> <div class="container" ng-non-bindable>
<h1>Chapter Permissions</h1> <h1>{{ trans('entities.chapters_permissions') }}</h1>
@include('form/restriction-form', ['model' => $chapter]) @include('form/restriction-form', ['model' => $chapter])
</div> </div>

View File

@ -6,30 +6,28 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-sm-8 faded" ng-non-bindable> <div class="col-sm-8 faded" ng-non-bindable>
<div class="breadcrumbs"> @include('chapters._breadcrumbs', ['chapter' => $chapter])
<a href="{{ $book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a>
</div>
</div> </div>
<div class="col-sm-4 faded"> <div class="col-sm-4 faded">
<div class="action-buttons"> <div class="action-buttons">
@if(userCan('page-create', $chapter)) @if(userCan('page-create', $chapter))
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>New Page</a> <a href="{{ $chapter->getUrl('/create-page') }}" class="text-pos text-button"><i class="zmdi zmdi-plus"></i>{{ trans('entities.pages_new') }}</a>
@endif @endif
@if(userCan('chapter-update', $chapter)) @if(userCan('chapter-update', $chapter))
<a href="{{ $chapter->getUrl('/edit') }}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>Edit</a> <a href="{{ $chapter->getUrl('/edit') }}" class="text-primary text-button"><i class="zmdi zmdi-edit"></i>{{ trans('common.edit') }}</a>
@endif @endif
@if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter)) @if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
<div dropdown class="dropdown-container"> <div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a> <a dropdown-toggle class="text-primary text-button"><i class="zmdi zmdi-more-vert"></i></a>
<ul> <ul>
@if(userCan('chapter-update', $chapter)) @if(userCan('chapter-update', $chapter))
<li><a href="{{ $chapter->getUrl('/move') }}" class="text-primary"><i class="zmdi zmdi-folder"></i>Move</a></li> <li><a href="{{ $chapter->getUrl('/move') }}" class="text-primary"><i class="zmdi zmdi-folder"></i>{{ trans('common.move') }}</a></li>
@endif @endif
@if(userCan('restrictions-manage', $chapter)) @if(userCan('restrictions-manage', $chapter))
<li><a href="{{ $chapter->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>Permissions</a></li> <li><a href="{{ $chapter->getUrl('/permissions') }}" class="text-primary"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.permissions') }}</a></li>
@endif @endif
@if(userCan('chapter-delete', $chapter)) @if(userCan('chapter-delete', $chapter))
<li><a href="{{ $chapter->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>Delete</a></li> <li><a href="{{ $chapter->getUrl('/delete') }}" class="text-neg"><i class="zmdi zmdi-delete"></i>{{ trans('common.delete') }}</a></li>
@endif @endif
</ul> </ul>
</div> </div>
@ -57,26 +55,22 @@
</div> </div>
@else @else
<hr> <hr>
<p class="text-muted">No pages are currently in this chapter.</p> <p class="text-muted">{{ trans('entities.chapters_empty') }}</p>
<p> <p>
@if(userCan('page-create', $chapter)) @if(userCan('page-create', $chapter))
<a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>Create a new page</a> <a href="{{ $chapter->getUrl('/create-page') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
@endif @endif
@if(userCan('page-create', $chapter) && userCan('book-update', $book)) @if(userCan('page-create', $chapter) && userCan('book-update', $book))
&nbsp;&nbsp;<em class="text-muted">-or-</em>&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
@endif @endif
@if(userCan('book-update', $book)) @if(userCan('book-update', $book))
<a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>Sort the current book</a> <a href="{{ $book->getUrl('/sort') }}" class="text-book"><i class="zmdi zmdi-book"></i>{{ trans('entities.books_empty_sort_current_book') }}</a>
@endif @endif
</p> </p>
<hr> <hr>
@endif @endif
<p class="text-muted small"> @include('partials.entity-meta', ['entity' => $chapter])
Created {{ $chapter->created_at->diffForHumans() }} @if($chapter->createdBy) by <a href="{{ $chapter->createdBy->getProfileUrl() }}">{{ $chapter->createdBy->name}}</a> @endif
<br>
Last Updated {{ $chapter->updated_at->diffForHumans() }} @if($chapter->updatedBy) by <a href="{{ $chapter->updatedBy->getProfileUrl() }}">{{ $chapter->updatedBy->name}}</a> @endif
</p>
</div> </div>
<div class="col-md-3 col-md-offset-1"> <div class="col-md-3 col-md-offset-1">
<div class="margin-top large"></div> <div class="margin-top large"></div>
@ -84,19 +78,20 @@
<div class="text-muted"> <div class="text-muted">
@if($book->restricted) @if($book->restricted)
<p class="text-muted">
@if(userCan('restrictions-manage', $book)) @if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Book Permissions Active</a> <a href="{{ $book->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}</a>
@else @else
<i class="zmdi zmdi-lock-outline"></i>Book Permissions Active <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.books_permissions_active') }}
@endif @endif
<br> </p>
@endif @endif
@if($chapter->restricted) @if($chapter->restricted)
@if(userCan('restrictions-manage', $chapter)) @if(userCan('restrictions-manage', $chapter))
<a href="{{ $chapter->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active</a> <a href="{{ $chapter->getUrl('/permissions') }}"><i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.chapters_permissions_active') }}</a>
@else @else
<i class="zmdi zmdi-lock-outline"></i>Chapter Permissions Active <i class="zmdi zmdi-lock-outline"></i>{{ trans('entities.chapters_permissions_active') }}
@endif @endif
@endif @endif
</div> </div>

View File

@ -2,12 +2,12 @@
<div class="overlay" entity-link-selector> <div class="overlay" entity-link-selector>
<div class="popup-body small flex-child"> <div class="popup-body small flex-child">
<div class="popup-header primary-background"> <div class="popup-header primary-background">
<div class="popup-title">Entity Select</div> <div class="popup-title">{{ trans('entities.entity_select') }}</div>
<button type="button" class="corner-button neg button popup-close">x</button> <button type="button" class="corner-button neg button popup-close">x</button>
</div> </div>
@include('partials/entity-selector', ['name' => 'entity-selector']) @include('components.entity-selector', ['name' => 'entity-selector'])
<div class="popup-footer"> <div class="popup-footer">
<button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">Select</button> <button type="button" disabled="true" class="button entity-link-selector-confirm pos corner-button">{{ trans('common.select') }}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
<div class="form-group"> <div class="form-group">
<div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}"> <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
<input type="hidden" entity-selector-input name="{{$name}}" value=""> <input type="hidden" entity-selector-input name="{{$name}}" value="">
<input type="text" placeholder="Search" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()"> <input type="text" placeholder="{{ trans('common.search') }}" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
<div class="text-center loading" ng-show="loading">@include('partials/loading-icon')</div> <div class="text-center loading" ng-show="loading">@include('partials.loading-icon')</div>
<div ng-show="!loading" ng-bind-html="entityResults"></div> <div ng-show="!loading" ng-bind-html="entityResults"></div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
<div class="popup-body" ng-click="$event.stopPropagation()"> <div class="popup-body" ng-click="$event.stopPropagation()">
<div class="popup-header primary-background"> <div class="popup-header primary-background">
<div class="popup-title">Image Select</div> <div class="popup-title">{{ trans('components.image_select') }}</div>
<button class="popup-close neg corner-button button">x</button> <button class="popup-close neg corner-button button">x</button>
</div> </div>
@ -12,16 +12,16 @@
<div class="image-manager-content"> <div class="image-manager-content">
<div ng-if="imageType === 'gallery'" class="container"> <div ng-if="imageType === 'gallery'" class="container">
<div class="image-manager-header row faded-small nav-tabs"> <div class="image-manager-header row faded-small nav-tabs">
<div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div> <div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div>
<div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div> <div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div>
<div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div> <div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div>
</div> </div>
</div> </div>
<div ng-show="view === 'all'" > <div ng-show="view === 'all'" >
<form ng-submit="searchImages()" class="contained-search-box"> <form ng-submit="searchImages()" class="contained-search-box">
<input type="text" placeholder="Search by image name" ng-model="searchTerm"> <input type="text" placeholder="{{ trans('components.image_search_hint') }}" ng-model="searchTerm">
<button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button> <button ng-class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
<button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button> <button title="{{ trans('common.search') }}" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
</form> </form>
</div> </div>
<div class="image-manager-list"> <div class="image-manager-list">
@ -31,11 +31,11 @@
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"> <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
<div class="image-meta"> <div class="image-meta">
<span class="name" ng-bind="image.name"></span> <span class="name" ng-bind="image.name"></span>
<span class="date">Uploaded @{{ getDate(image.created_at) }}</span> <span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div> <div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.image_load_more') }}</div>
</div> </div>
</div> </div>
@ -51,15 +51,14 @@
</a> </a>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="name">Image Name</label> <label for="name">{{ trans('components.image_image_name') }}</label>
<input type="text" id="name" name="name" ng-model="selectedImage.name"> <input type="text" id="name" name="name" ng-model="selectedImage.name">
</div> </div>
</form> </form>
<div ng-show="dependantPages"> <div ng-show="dependantPages">
<p class="text-neg text-small"> <p class="text-neg text-small">
This image is used in the pages below, Click delete again to confirm you want to delete {{ trans('components.image_delete_confirm') }}
this image.
</p> </p>
<ul class="text-neg"> <ul class="text-neg">
<li ng-repeat="page in dependantPages"> <li ng-repeat="page in dependantPages">
@ -73,13 +72,13 @@
<button class="button icon neg"><i class="zmdi zmdi-delete"></i></button> <button class="button icon neg"><i class="zmdi zmdi-delete"></i></button>
</form> </form>
<button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()"> <button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()">
<i class="zmdi zmdi-square-right"></i>Select Image <i class="zmdi zmdi-square-right"></i>{{ trans('components.image_select_image') }}
</button> </button>
</div> </div>
</div> </div>
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone> <drop-zone message="{{ trans('components.image_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
</div> </div>

View File

@ -0,0 +1,66 @@
<div class="image-picker" image-picker="{{$name}}" data-default-image="{{ $defaultImage }}" data-resize-height="{{ $resizeHeight }}" data-resize-width="{{ $resizeWidth }}" data-current-id="{{ $currentId or '' }}" data-resize-crop="{{ $resizeCrop or '' }}">
<div>
<img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
</div>
<button class="button" type="button" data-action="show-image-manager">{{ trans('components.image_select_image') }}</button>
<br>
<button class="text-button" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
@if ($showRemove)
<span class="sep">|</span>
<button class="text-button neg" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
@endif
<input type="hidden" name="{{$name}}" id="{{$name}}" value="{{ isset($currentId) && ($currentId !== '' && $currentId !== false) ? $currentId : $currentImage}}">
</div>
<script>
(function(){
var picker = document.querySelector('[image-picker="{{$name}}"]');
picker.addEventListener('click', function(event) {
if (event.target.nodeName.toLowerCase() !== 'button') return;
var button = event.target;
var action = button.getAttribute('data-action');
var resize = picker.getAttribute('data-resize-height') && picker.getAttribute('data-resize-width');
var usingIds = picker.getAttribute('data-current-id') !== '';
var resizeCrop = picker.getAttribute('data-resize-crop') !== '';
var imageElem = picker.querySelector('img');
var input = picker.querySelector('input');
function setImage(image) {
if (image === 'none') {
imageElem.src = picker.getAttribute('data-default-image');
imageElem.classList.add('none');
input.value = 'none';
return;
}
imageElem.src = image.url;
input.value = usingIds ? image.id : image.url;
imageElem.classList.remove('none');
}
if (action === 'show-image-manager') {
window.ImageManager.showExternal((image) => {
if (!resize) {
setImage(image);
return;
}
var requestString = '/images/thumb/' + image.id + '/' + picker.getAttribute('data-resize-width') + '/' + picker.getAttribute('data-resize-height') + '/' + (resizeCrop ? 'true' : 'false');
$.get(window.baseUrl(requestString), resp => {
image.url = resp.url;
setImage(image);
});
});
} else if (action === 'reset-image') {
setImage({id: 0, url: picker.getAttribute('data-default-image')});
} else if (action === 'remove-image') {
setImage('none');
}
});
})();
</script>

View File

@ -0,0 +1,15 @@
<div toggle-switch="{{$name}}" class="toggle-switch @if($value) active @endif">
<input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
<div class="switch-handle"></div>
</div>
<script>
(function() {
var toggle = document.querySelector('[toggle-switch="{{$name}}"]');
var toggleInput = toggle.querySelector('input');
toggle.onclick = function(event) {
var checked = toggleInput.value !== 'true';
toggleInput.value = checked ? 'true' : 'false';
checked ? toggle.classList.add('active') : toggle.classList.remove('active');
};
})()
</script>

View File

@ -4,9 +4,28 @@
<div class="container"> <div class="container">
<h1 class="text-muted">{{ $message or 'Page Not Found' }}</h1>
<p>Sorry, The page you were looking for could not be found.</p>
<a href="{{ baseUrl('/') }}" class="button">Return To Home</a> <h1>{{ $message or trans('errors.404_page_not_found') }}</h1>
<p>{{ trans('errors.sorry_page_not_found') }}</p>
<p><a href="{{ baseUrl('/') }}" class="button">{{ trans('errors.return_home') }}</a></p>
<hr>
<div class="row">
<div class="col-md-4">
<h3 class="text-muted">{{ trans('entities.pages_popular') }}</h3>
@include('partials.entity-list', ['entities' => Views::getPopular(10, 0, [\BookStack\Page::class]), 'style' => 'compact'])
</div>
<div class="col-md-4">
<h3 class="text-muted">{{ trans('entities.books_popular') }}</h3>
@include('partials.entity-list', ['entities' => Views::getPopular(10, 0, [\BookStack\Book::class]), 'style' => 'compact'])
</div>
<div class="col-md-4">
<h3 class="text-muted">{{ trans('entities.chapters_popular') }}</h3>
@include('partials.entity-list', ['entities' => Views::getPopular(10, 0, [\BookStack\Chapter::class]), 'style' => 'compact'])
</div>
</div>
</div> </div>
@stop @stop

View File

@ -3,7 +3,7 @@
@section('content') @section('content')
<div class="container"> <div class="container">
<h1 class="text-muted">An Error Occurred</h1> <h1 class="text-muted">{{ trans('errors.error_occurred') }}</h1>
<p>{{ $message }}</p> <p>{{ $message }}</p>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More