diff --git a/.env.example b/.env.example index ddb32c0bc..80afb6274 100644 --- a/.env.example +++ b/.env.example @@ -47,10 +47,15 @@ GITHUB_APP_SECRET=false GOOGLE_APP_ID=false GOOGLE_APP_SECRET=false OKTA_BASE_URL=false -OKTA_KEY=false -OKTA_SECRET=false +OKTA_APP_ID=false +OKTA_APP_SECRET=false +TWITCH_APP_ID=false +TWITCH_APP_SECRET=false +GITLAB_APP_ID=false +GITLAB_APP_SECRET=false +GITLAB_BASE_URI=false -# External services such as Gravatar +# External services such as Gravatar and Draw.IO DISABLE_EXTERNAL_SERVICES=false # LDAP Settings @@ -67,4 +72,4 @@ MAIL_HOST=localhost MAIL_PORT=1025 MAIL_USERNAME=null MAIL_PASSWORD=null -MAIL_ENCRYPTION=null \ No newline at end of file +MAIL_ENCRYPTION=null diff --git a/app/Activity.php b/app/Activity.php index af386700a..c01da1f6c 100644 --- a/app/Activity.php +++ b/app/Activity.php @@ -16,7 +16,9 @@ class Activity extends Model */ public function entity() { - if ($this->entity_type === '') $this->entity_type = null; + if ($this->entity_type === '') { + $this->entity_type = null; + } return $this->morphTo('entity'); } @@ -43,8 +45,8 @@ class Activity extends Model * @param $activityB * @return bool */ - public function isSimilarTo($activityB) { + public function isSimilarTo($activityB) + { return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id]; } - } diff --git a/app/Attachment.php b/app/Attachment.php index fe291bec2..55344cd7d 100644 --- a/app/Attachment.php +++ b/app/Attachment.php @@ -1,6 +1,5 @@ name, '.')) return $this->name; + if (str_contains($this->name, '.')) { + return $this->name; + } return $this->name . '.' . $this->extension; } @@ -32,5 +33,4 @@ class Attachment extends Ownable { return baseUrl('/attachments/' . $this->id); } - } diff --git a/app/Book.php b/app/Book.php index 3fb87b4c5..457a4c928 100644 --- a/app/Book.php +++ b/app/Book.php @@ -27,7 +27,9 @@ class Book extends Entity public function getBookCover($width = 440, $height = 250) { $default = baseUrl('/book_default_cover.png'); - if (!$this->image_id) return $default; + if (!$this->image_id) { + return $default; + } try { $cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default; @@ -91,5 +93,4 @@ class Book extends Entity { return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at"; } - } diff --git a/app/Chapter.php b/app/Chapter.php index b08cb913a..6dab9dc47 100644 --- a/app/Chapter.php +++ b/app/Chapter.php @@ -1,6 +1,5 @@ textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at"; } - } diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php new file mode 100644 index 000000000..c7a9969e8 --- /dev/null +++ b/app/Console/Commands/CreateAdmin.php @@ -0,0 +1,85 @@ +userRepo = $userRepo; + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + * @throws \BookStack\Exceptions\NotFoundException + */ + public function handle() + { + $email = trim($this->option('email')); + if (empty($email)) { + $email = $this->ask('Please specify an email address for the new admin user'); + } + if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $this->error('Invalid email address provided'); + } + + if ($this->userRepo->getByEmail($email) !== null) { + return $this->error('A user with the provided email already exists!'); + } + + $name = trim($this->option('name')); + if (empty($name)) { + $name = $this->ask('Please specify an name for the new admin user'); + } + if (strlen($name) < 2) { + return $this->error('Invalid name provided'); + } + + $password = trim($this->option('password')); + if (empty($password)) { + $password = $this->secret('Please specify a password for the new admin user'); + } + if (strlen($password) < 5) { + return $this->error('Invalid password provided, Must be at least 5 characters'); + } + + + $user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]); + $this->userRepo->attachSystemRole($user, 'admin'); + $this->userRepo->downloadGravatarToUserAvatar($user); + $user->email_confirmed = true; + $user->save(); + + $this->info("Admin account with email \"{$user->email}\" successfully created!"); + } +} diff --git a/app/Console/Commands/DeleteUsers.php b/app/Console/Commands/DeleteUsers.php new file mode 100644 index 000000000..6dba83e13 --- /dev/null +++ b/app/Console/Commands/DeleteUsers.php @@ -0,0 +1,57 @@ +user = $user; + $this->userRepo = $userRepo; + parent::__construct(); + } + + public function handle() + { + $confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)'); + $numDeleted = 0; + if (strtolower(trim($confirm)) === 'yes') { + $totalUsers = $this->user->count(); + $users = $this->user->where('system_name', '=', null)->with('roles')->get(); + foreach ($users as $user) { + if ($user->hasSystemRole('admin')) { + // don't delete users with "admin" role + continue; + } + $this->userRepo->destroy($user); + ++$numDeleted; + } + $this->info("Deleted $numDeleted of $totalUsers total users."); + } else { + $this->info('Exiting...'); + } + } +} diff --git a/app/Entity.php b/app/Entity.php index df8e4d38b..1ea4e8dac 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -1,6 +1,5 @@ id] === [get_class($entity), $entity->id]; - if ($matches) return true; + if ($matches) { + return true; + } if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) { return $entity->book_id === $this->id; @@ -159,7 +160,9 @@ class Entity extends Ownable */ public function getShortName($length = 25) { - if (strlen($this->name) <= $length) return $this->name; + if (strlen($this->name) <= $length) { + return $this->name; + } return substr($this->name, 0, $length - 3) . '...'; } @@ -176,13 +179,18 @@ class Entity extends Ownable * Return a generalised, common raw query that can be 'unioned' across entities. * @return string */ - public function entityRawQuery(){return '';} + public function entityRawQuery() + { + return ''; + } /** * Get the url of this entity * @param $path * @return string */ - public function getUrl($path){return '/';} - + public function getUrl($path) + { + return '/'; + } } diff --git a/app/EntityPermission.php b/app/EntityPermission.php index eaf0a8951..0f49e07f5 100644 --- a/app/EntityPermission.php +++ b/app/EntityPermission.php @@ -1,6 +1,5 @@ view('errors/' . $code, ['message' => $message], $code); } + // Handle 404 errors with a loaded session to enable showing user-specific information + if ($this->isExceptionType($e, NotFoundHttpException::class)) { + return $this->loadErrorMiddleware($request, function ($request) use ($e) { + $message = $e->getMessage() ?: trans('errors.404_page_not_found'); + return response()->view('errors/404', ['message' => $message], 404); + }); + } + return parent::render($request, $e); } + /** + * Load the middleware required to show state/session-enabled error pages. + * @param Request $request + * @param $callback + * @return mixed + */ + protected function loadErrorMiddleware(Request $request, $callback) + { + $middleware = (\Route::getMiddlewareGroups()['web_errors']); + return (new Pipeline($this->container)) + ->send($request) + ->through($middleware) + ->then($callback); + } + /** * Check the exception chain to compare against the original exception type. * @param Exception $e * @param $type * @return bool */ - protected function isExceptionType(Exception $e, $type) { + protected function isExceptionType(Exception $e, $type) + { do { - if (is_a($e, $type)) return true; + if (is_a($e, $type)) { + return true; + } } while ($e = $e->getPrevious()); return false; } @@ -81,7 +110,8 @@ class Handler extends ExceptionHandler * @param Exception $e * @return string */ - protected function getOriginalMessage(Exception $e) { + protected function getOriginalMessage(Exception $e) + { do { $message = $e->getMessage(); } while ($e = $e->getPrevious()); diff --git a/app/Exceptions/ImageUploadException.php b/app/Exceptions/ImageUploadException.php index 6f4c73037..c64dddaa2 100644 --- a/app/Exceptions/ImageUploadException.php +++ b/app/Exceptions/ImageUploadException.php @@ -1,3 +1,6 @@ redirectLocation = $redirectLocation; parent::__construct(); } -} \ No newline at end of file +} diff --git a/app/Exceptions/PermissionsException.php b/app/Exceptions/PermissionsException.php index ae4c676d6..e2a8c53b4 100644 --- a/app/Exceptions/PermissionsException.php +++ b/app/Exceptions/PermissionsException.php @@ -1,6 +1,8 @@ attachment->findOrFail($attachmentId); $page = $this->entityRepo->getById('page', $attachment->uploaded_to); + if ($page === null) { + throw new NotFoundException(trans('errors.attachment_not_found')); + } + $this->checkOwnablePermission('page-view', $page); if ($attachment->external) { @@ -204,6 +210,7 @@ class AttachmentController extends Controller * Delete a specific attachment in the system. * @param $attachmentId * @return mixed + * @throws \Exception */ public function delete($attachmentId) { diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index d1fbddc50..a0cbae9c6 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -64,5 +64,4 @@ class ForgotPasswordController extends Controller ['email' => trans($response)] ); } - -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 3617652c2..106b90524 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -70,7 +70,9 @@ class LoginController extends Controller protected function authenticated(Request $request, Authenticatable $user) { // Explicitly log them out for now if they do no exist. - if (!$user->exists) auth()->logout($user); + if (!$user->exists) { + auth()->logout($user); + } if (!$user->exists && $user->email === null && !$request->filled('email')) { $request->flash(); @@ -83,7 +85,6 @@ class LoginController extends Controller } if (!$user->exists) { - // Check for users with same email already $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0; if ($alreadyUser) { @@ -130,4 +131,4 @@ class LoginController extends Controller session()->put('social-callback', 'login'); return $this->socialAuthService->startLogIn($socialDriver); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 5a7a5e971..1bbd5e2ba 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -91,6 +91,7 @@ class RegisterController extends Controller /** * Show the application registration form. * @return Response + * @throws UserRegistrationException */ public function getRegister() { @@ -102,20 +103,13 @@ class RegisterController extends Controller /** * Handle a registration request for the application. * @param Request|\Illuminate\Http\Request $request - * @return Response + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException - * @throws \Illuminate\Validation\ValidationException */ public function postRegister(Request $request) { $this->checkRegistrationAllowed(); - $validator = $this->validator($request->all()); - - if ($validator->fails()) { - $this->throwValidationException( - $request, $validator - ); - } + $this->validator($request->all())->validate(); $userData = $request->all(); return $this->registerUser($userData); @@ -141,7 +135,6 @@ class RegisterController extends Controller * @param bool|false|SocialAccount $socialAccount * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException - * @throws ConfirmationEmailException */ protected function registerUser(array $userData, $socialAccount = false) { @@ -239,6 +232,8 @@ class RegisterController extends Controller * Redirect to the social site for authentication intended to register. * @param $socialDriver * @return mixed + * @throws UserRegistrationException + * @throws \BookStack\Exceptions\SocialDriverNotConfigured */ public function socialRegister($socialDriver) { @@ -272,8 +267,12 @@ class RegisterController extends Controller } $action = session()->pull('social-callback'); - if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver); - if ($action == 'register') return $this->socialRegisterCallback($socialDriver); + if ($action == 'login') { + return $this->socialAuthService->handleLoginCallback($socialDriver); + } + if ($action == 'register') { + return $this->socialRegisterCallback($socialDriver); + } return redirect()->back(); } @@ -291,7 +290,6 @@ class RegisterController extends Controller * Register a new user after a registration callback. * @param $socialDriver * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * @throws ConfirmationEmailException * @throws UserRegistrationException * @throws \BookStack\Exceptions\SocialDriverNotConfigured */ @@ -308,5 +306,4 @@ class RegisterController extends Controller ]; return $this->registerUser($userData, $socialAccount); } - -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index eb678503d..56f1cf026 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -46,4 +46,4 @@ class ResetPasswordController extends Controller return redirect($this->redirectPath()) ->with('status', trans($response)); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index e181aec89..2c3946239 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -36,17 +36,17 @@ class BookController extends Controller */ public function index() { - $books = $this->entityRepo->getAllPaginated('book', 20); + $books = $this->entityRepo->getAllPaginated('book', 18); $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false; $popular = $this->entityRepo->getPopular('book', 4, 0); $new = $this->entityRepo->getRecentlyCreated('book', 4, 0); - $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', 'list'); + $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); $this->setPageTitle(trans('entities.books')); return view('books/index', [ 'books' => $books, 'recents' => $recents, 'popular' => $popular, - 'new' => $new, + 'new' => $new, 'booksViewType' => $booksViewType ]); } @@ -109,7 +109,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $slug); $this->checkOwnablePermission('book-update', $book); - $this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()])); + $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()])); return view('books/edit', ['book' => $book, 'current' => $book]); } @@ -155,7 +155,7 @@ class BookController extends Controller $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-update', $book); $bookChildren = $this->entityRepo->getBookChildren($book, true); - $books = $this->entityRepo->getAll('book', false); + $books = $this->entityRepo->getAll('book', false, 'update'); $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()])); return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); } @@ -190,42 +190,56 @@ class BookController extends Controller } // Sort pages and chapters - $sortedBooks = []; - $updatedModels = collect(); - $sortMap = json_decode($request->get('sort-tree')); - $defaultBookId = $book->id; + $sortMap = collect(json_decode($request->get('sort-tree'))); + $bookIdsInvolved = collect([$book->id]); - // Loop through contents of provided map and update entities accordingly - foreach ($sortMap as $bookChild) { - $priority = $bookChild->sort; - $id = intval($bookChild->id); - $isPage = $bookChild->type == 'page'; - $bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId; - $chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter); - $model = $this->entityRepo->getById($isPage?'page':'chapter', $id); + // Load models into map + $sortMap->each(function ($mapItem) use ($bookIdsInvolved) { + $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter'); + $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id); + // Store source and target books + $bookIdsInvolved->push(intval($mapItem->model->book_id)); + $bookIdsInvolved->push(intval($mapItem->book)); + }); - // 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)) { - $this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model); - $model->priority = $priority; - if ($isPage) $model->chapter_id = $chapterId; + // Get the books involved in the sort + $bookIdsInvolved = $bookIdsInvolved->unique()->toArray(); + $booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get(); + // Throw permission error if invalid ids or inaccessible books given. + if (count($bookIdsInvolved) !== count($booksInvolved)) { + $this->showPermissionError(); + } + // Check permissions of involved books + $booksInvolved->each(function (Book $book) { + $this->checkOwnablePermission('book-update', $book); + }); + + // Perform the sort + $sortMap->each(function ($mapItem) { + $model = $mapItem->model; + + $priorityChanged = intval($model->priority) !== intval($mapItem->sort); + $bookChanged = intval($model->book_id) !== intval($mapItem->book); + $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter; + + if ($bookChanged) { + $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model); + } + if ($chapterChanged) { + $model->chapter_id = intval($mapItem->parentChapter); $model->save(); - $updatedModels->push($model); } - - // Store involved books to be sorted later - if (!in_array($bookId, $sortedBooks)) { - $sortedBooks[] = $bookId; + if ($priorityChanged) { + $model->priority = intval($mapItem->sort); + $model->save(); } - } + }); - // Add activity for books - foreach ($sortedBooks as $bookId) { - /** @var Book $updatedBook */ - $updatedBook = $this->entityRepo->getById('book', $bookId); - $this->entityRepo->buildJointPermissionsForBook($updatedBook); - Activity::add($updatedBook, 'book_sort', $updatedBook->id); - } + // Rebuild permissions and add activity for involved books. + $booksInvolved->each(function (Book $book) { + $this->entityRepo->buildJointPermissionsForBook($book); + Activity::add($book, 'book_sort', $book->id); + }); return redirect($book->getUrl()); } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index ceeb2a3ef..a4e0b6409 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -159,7 +159,8 @@ class ChapterController extends Controller * @return mixed * @throws \BookStack\Exceptions\NotFoundException */ - public function showMove($bookSlug, $chapterSlug) { + public function showMove($bookSlug, $chapterSlug) + { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()])); $this->checkOwnablePermission('chapter-update', $chapter); @@ -177,7 +178,8 @@ class ChapterController extends Controller * @return mixed * @throws \BookStack\Exceptions\NotFoundException */ - public function move($bookSlug, $chapterSlug, Request $request) { + public function move($bookSlug, $chapterSlug, Request $request) + { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $this->checkOwnablePermission('chapter-update', $chapter); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 733d5416b..a51ff5d77 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -51,7 +51,9 @@ abstract class Controller extends BaseController */ protected function preventAccessForDemoUsers() { - if (config('app.env') === 'demo') $this->showPermissionError(); + if (config('app.env') === 'demo') { + $this->showPermissionError(); + } } /** @@ -100,7 +102,9 @@ abstract class Controller extends BaseController */ protected function checkOwnablePermission($permission, Ownable $ownable) { - if (userCan($permission, $ownable)) return true; + if (userCan($permission, $ownable)) { + return true; + } return $this->showPermissionError(); } @@ -113,7 +117,9 @@ abstract class Controller extends BaseController protected function checkPermissionOr($permissionName, $callback) { $callbackResult = $callback(); - if ($callbackResult === false) $this->checkPermission($permissionName); + if ($callbackResult === false) { + $this->checkPermission($permissionName); + } return true; } @@ -145,5 +151,4 @@ abstract class Controller extends BaseController ->withInput($request->input()) ->withErrors($errors, $this->errorBag()); } - } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 164becd4d..b2620588f 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -56,7 +56,8 @@ class HomeController extends Controller * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response * @throws \Exception */ - public function getTranslations() { + public function getTranslations() + { $locale = app()->getLocale(); $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale; if (cache()->has($cacheKey) && config('app.env') !== 'development') { @@ -95,5 +96,4 @@ class HomeController extends Controller { return view('partials/custom-head-content'); } - } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index d40f88255..a61f447eb 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -1,6 +1,7 @@ file($path); + } + /** * Get all images for a specific type, Paginated * @param string $type @@ -47,14 +63,14 @@ class ImageController extends Controller * @param Request $request * @return mixed */ - public function searchByType($type, $page = 0, Request $request) + public function searchByType(Request $request, $type, $page = 0) { $this->validate($request, [ 'term' => 'required|string' ]); $searchTerm = $request->get('term'); - $imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm); + $imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24); return response()->json($imgData); } @@ -76,17 +92,19 @@ class ImageController extends Controller * @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(Request $request, $filter, $page = 0) { $this->validate($request, [ 'page_id' => 'required|integer' ]); $validFilters = collect(['page', 'book']); - if (!$validFilters->contains($filter)) return response('Invalid filter', 500); + if (!$validFilters->contains($filter)) { + return response('Invalid filter', 500); + } $pageId = $request->get('page_id'); - $imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId); + $imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24); return response()->json($imgData); } @@ -96,6 +114,7 @@ class ImageController extends Controller * @param string $type * @param Request $request * @return \Illuminate\Http\JsonResponse + * @throws \Exception */ public function uploadByType($type, Request $request) { @@ -104,10 +123,14 @@ class ImageController extends Controller 'file' => 'is_image' ]); + if (!$this->imageRepo->isValidType($type)) { + return $this->jsonError(trans('errors.image_upload_type_error')); + } + $imageUpload = $request->file('file'); try { - $uploadedTo = $request->filled('uploaded_to') ? $request->get('uploaded_to') : 0; + $uploadedTo = $request->get('uploaded_to', 0); $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); @@ -116,6 +139,73 @@ class ImageController extends Controller return response()->json($image); } + /** + * Upload a drawing to the system. + * @param Request $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function uploadDrawing(Request $request) + { + $this->validate($request, [ + 'image' => 'required|string', + 'uploaded_to' => 'required|integer' + ]); + $this->checkPermission('image-create-all'); + $imageBase64Data = $request->get('image'); + + try { + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + + /** + * Replace the data content of a drawing. + * @param string $id + * @param Request $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function replaceDrawing(string $id, Request $request) + { + $this->validate($request, [ + 'image' => 'required|string' + ]); + $this->checkPermission('image-create-all'); + + $imageBase64Data = $request->get('image'); + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('image-update', $image); + + try { + $image = $this->imageRepo->replaceDrawingContent($image, $imageBase64Data); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + + /** + * Get the content of an image based64 encoded. + * @param $id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getBase64Image($id) + { + $image = $this->imageRepo->getById($id); + $imageData = $this->imageRepo->getImageData($image); + if ($imageData === null) { + return $this->jsonError("Image data could not be found"); + } + return response()->json([ + 'content' => base64_encode($imageData) + ]); + } + /** * Generate a sized thumbnail for an image. * @param $id @@ -123,6 +213,8 @@ class ImageController extends Controller * @param $height * @param $crop * @return \Illuminate\Http\JsonResponse + * @throws ImageUploadException + * @throws \Exception */ public function getThumbnail($id, $width, $height, $crop) { @@ -137,6 +229,8 @@ class ImageController extends Controller * @param integer $imageId * @param Request $request * @return \Illuminate\Http\JsonResponse + * @throws ImageUploadException + * @throws \Exception */ public function update($imageId, Request $request) { @@ -173,6 +267,4 @@ class ImageController extends Controller $this->imageRepo->destroyImage($image); return response()->json(trans('components.images_deleted')); } - - } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 13e928465..17bce7eba 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -145,6 +145,7 @@ class PageController extends Controller * @param string $bookSlug * @param string $pageSlug * @return Response + * @throws NotFoundException */ public function show($bookSlug, $pageSlug) { @@ -152,7 +153,9 @@ class PageController extends Controller $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); } catch (NotFoundException $e) { $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug); - if ($page === null) abort(404); + if ($page === null) { + throw $e; + } return redirect($page->getUrl()); } @@ -219,7 +222,9 @@ class PageController extends Controller $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)); + } $draftsEnabled = $this->signedIn; return view('pages/edit', [ @@ -603,5 +608,4 @@ class PageController extends Controller session()->flash('success', trans('entities.pages_permissions_success')); return redirect($page->getUrl()); } - } diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php index cd064e7e8..c4c7fe972 100644 --- a/app/Http/Controllers/PermissionController.php +++ b/app/Http/Controllers/PermissionController.php @@ -67,7 +67,9 @@ class PermissionController extends Controller { $this->checkPermission('user-roles-manage'); $role = $this->permissionsRepo->getRoleById($id); - if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited')); + if ($role->hidden) { + throw new PermissionsException(trans('errors.role_cannot_be_edited')); + } return view('settings/roles/edit', ['role' => $role]); } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 0faeb0dec..0827eeb71 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -104,7 +104,4 @@ class SearchController extends Controller return view('search/entity-ajax-list', ['entities' => $entities]); } - } - - diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 70a12631a..e0e351458 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -33,7 +33,9 @@ class SettingController extends Controller // Cycles through posted settings and update them foreach ($request->all() as $name => $value) { - if (strpos($name, 'setting-') !== 0) continue; + if (strpos($name, 'setting-') !== 0) { + continue; + } $key = str_replace('setting-', '', trim($name)); Setting::put($key, $value); } @@ -41,5 +43,4 @@ class SettingController extends Controller session()->flash('success', trans('settings.settings_save_success')); return redirect('/settings'); } - } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 5cf7a0f8f..815d9e8d5 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -54,5 +54,4 @@ class TagController extends Controller $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); return response()->json($suggestions); } - } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index fe5c7a243..d50baa86f 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -93,17 +93,7 @@ class UserController extends Controller $user->roles()->sync($roles); } - // Get avatar from gravatar and save - if (!config('services.disable_services')) { - try { - $avatar = \Images::saveUserGravatar($user); - $user->avatar()->associate($avatar); - $user->save(); - } catch (Exception $e) { - \Log::error('Failed to save user gravatar image'); - } - - } + $this->userRepo->downloadGravatarToUserAvatar($user); return redirect('/settings/users'); } @@ -249,4 +239,27 @@ class UserController extends Controller 'assetCounts' => $assetCounts ]); } + + /** + * Update the user's preferred book-list display setting. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function switchBookView($id, Request $request) + { + $this->checkPermissionOr('users-manage', function () use ($id) { + return $this->currentUser->id == $id; + }); + + $viewType = $request->get('book_view_type'); + if (!in_array($viewType, ['grid', 'list'])) { + $viewType = 'list'; + } + + $user = $this->user->findOrFail($id); + setting()->putUser($user, 'books_view_type', $viewType); + + return redirect()->back(302, [], "/settings/users/$id"); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index cd894de95..9d2871bbe 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -33,6 +33,14 @@ class Kernel extends HttpKernel \Illuminate\Routing\Middleware\SubstituteBindings::class, \BookStack\Http\Middleware\Localization::class ], + 'web_errors' => [ + \BookStack\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \BookStack\Http\Middleware\VerifyCsrfToken::class, + \BookStack\Http\Middleware\Localization::class + ], 'api' => [ 'throttle:60,1', 'bindings', diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php index 14c87c377..466c1442b 100644 --- a/app/Http/Middleware/Localization.php +++ b/app/Http/Middleware/Localization.php @@ -19,7 +19,9 @@ class Localization $locale = $defaultLang; $availableLocales = config('app.locales'); foreach ($request->getLanguages() as $lang) { - if (!in_array($lang, $availableLocales)) continue; + if (!in_array($lang, $availableLocales)) { + continue; + } $locale = $lang; break; } diff --git a/app/Model.php b/app/Model.php index 9ec2b7362..498bacb20 100644 --- a/app/Model.php +++ b/app/Model.php @@ -15,5 +15,4 @@ class Model extends EloquentModel { return parent::getAttributeFromArray($key); } - -} \ No newline at end of file +} diff --git a/app/Notifications/ConfirmEmail.php b/app/Notifications/ConfirmEmail.php index 27ac89c32..858b12166 100644 --- a/app/Notifications/ConfirmEmail.php +++ b/app/Notifications/ConfirmEmail.php @@ -49,5 +49,4 @@ class ConfirmEmail extends Notification implements ShouldQueue ->line(trans('auth.email_confirm_text')) ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token)); } - } diff --git a/app/Ownable.php b/app/Ownable.php index f2cfe801b..fe58e05ed 100644 --- a/app/Ownable.php +++ b/app/Ownable.php @@ -1,6 +1,5 @@ textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at"; } - } diff --git a/app/PageRevision.php b/app/PageRevision.php index 0a9764729..ffcc4f9d2 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -1,6 +1,5 @@ page->getUrl() . '/revisions/' . $this->id; - if ($path) return $url . '/' . trim($path, '/'); + if ($path) { + return $url . '/' . trim($path, '/'); + } return $url; } @@ -58,5 +59,4 @@ class PageRevision extends Model { return $type === 'revision'; } - } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ef3ee6c48..b06b2f3a2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -15,12 +15,12 @@ class AppServiceProvider extends ServiceProvider public function boot() { // 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']; return in_array($value->getMimeType(), $imageMimes); }); - \Blade::directive('icon', function($expression) { + \Blade::directive('icon', function ($expression) { return ""; }); @@ -35,7 +35,7 @@ class AppServiceProvider extends ServiceProvider */ public function register() { - $this->app->singleton(SettingService::class, function($app) { + $this->app->singleton(SettingService::class, function ($app) { return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository')); }); } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 509b86182..d1fac56e6 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider */ public function register() { - Auth::provider('ldap', function($app, array $config) { + Auth::provider('ldap', function ($app, array $config) { return new LdapUserProvider($config['model'], $app[LdapService::class]); }); } diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php index b2c7acf5e..a97512e8c 100644 --- a/app/Providers/CustomFacadeProvider.php +++ b/app/Providers/CustomFacadeProvider.php @@ -34,28 +34,28 @@ class CustomFacadeProvider extends ServiceProvider */ public function register() { - $this->app->bind('activity', function() { + $this->app->bind('activity', function () { return new ActivityService( $this->app->make(Activity::class), $this->app->make(PermissionService::class) ); }); - $this->app->bind('views', function() { + $this->app->bind('views', function () { return new ViewService( $this->app->make(View::class), $this->app->make(PermissionService::class) ); }); - $this->app->bind('setting', function() { + $this->app->bind('setting', function () { return new SettingService( $this->app->make(Setting::class), $this->app->make(Repository::class) ); }); - $this->app->bind('images', function() { + $this->app->bind('images', function () { return new ImageService( $this->app->make(ImageManager::class), $this->app->make(Factory::class), diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 05f9c57c1..cc3e4d993 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -18,6 +18,8 @@ class EventServiceProvider extends ServiceProvider 'SocialiteProviders\Slack\SlackExtendSocialite@handle', 'SocialiteProviders\Azure\AzureExtendSocialite@handle', 'SocialiteProviders\Okta\OktaExtendSocialite@handle', + 'SocialiteProviders\GitLab\GitLabExtendSocialite@handle', + 'SocialiteProviders\Twitch\TwitchExtendSocialite@handle', ], ]; diff --git a/app/Providers/LdapUserProvider.php b/app/Providers/LdapUserProvider.php index a15257aec..1dc789c3b 100644 --- a/app/Providers/LdapUserProvider.php +++ b/app/Providers/LdapUserProvider.php @@ -2,7 +2,6 @@ namespace BookStack\Providers; - use BookStack\Role; use BookStack\Services\LdapService; use BookStack\User; @@ -102,7 +101,9 @@ class LdapUserProvider implements UserProvider { // Get user via LDAP $userDetails = $this->ldapService->getUserDetails($credentials['username']); - if ($userDetails === null) return null; + if ($userDetails === null) { + return null; + } // Search current user base by looking up a uid $model = $this->createModel(); @@ -110,7 +111,9 @@ class LdapUserProvider implements UserProvider ->where('external_auth_id', $userDetails['uid']) ->first(); - if ($currentUser !== null) return $currentUser; + if ($currentUser !== null) { + return $currentUser; + } $model->name = $userDetails['name']; $model->external_auth_id = $userDetails['uid']; diff --git a/app/Providers/PaginationServiceProvider.php b/app/Providers/PaginationServiceProvider.php index ec41267a4..3a695c5e3 100644 --- a/app/Providers/PaginationServiceProvider.php +++ b/app/Providers/PaginationServiceProvider.php @@ -1,6 +1,5 @@ id; $comment = $this->comment->newInstance($data); @@ -81,7 +82,9 @@ class CommentRepo { protected function getNextLocalId(Entity $entity) { $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first(); - if ($comments === null) return 1; + if ($comments === null) { + return 1; + } return $comments->local_id + 1; } -} \ No newline at end of file +} diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index c31ddfefe..64f7a0810 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -77,11 +77,15 @@ class EntityRepo * @param SearchService $searchService */ public function __construct( - Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision, - ViewService $viewService, PermissionService $permissionService, - TagRepo $tagRepo, SearchService $searchService - ) - { + Book $book, + Chapter $chapter, + Page $page, + PageRevision $pageRevision, + ViewService $viewService, + PermissionService $permissionService, + TagRepo $tagRepo, + SearchService $searchService + ) { $this->book = $book; $this->chapter = $chapter; $this->page = $page; @@ -113,9 +117,9 @@ class EntityRepo * @param bool $allowDrafts * @return \Illuminate\Database\Query\Builder */ - protected function entityQuery($type, $allowDrafts = false) + protected function entityQuery($type, $allowDrafts = false, $permission = 'view') { - $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view'); + $q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission); if (strtolower($type) === 'page' && !$allowDrafts) { $q = $q->where('draft', '=', false); } @@ -163,14 +167,16 @@ class EntityRepo $q = $this->entityQuery($type)->where('slug', '=', $slug); if (strtolower($type) === 'chapter' || strtolower($type) === 'page') { - $q = $q->where('book_id', '=', function($query) use ($bookSlug) { + $q = $q->where('book_id', '=', function ($query) use ($bookSlug) { $query->select('id') ->from($this->book->getTable()) ->where('slug', '=', $bookSlug)->limit(1); }); } $entity = $q->first(); - if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found')); + if ($entity === null) { + throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found')); + } return $entity; } @@ -196,15 +202,18 @@ class EntityRepo } /** - * Get all entities of a type limited by count unless count if false. + * Get all entities of a type with the given permission, limited by count unless count is false. * @param string $type * @param integer|bool $count + * @param string $permission * @return Collection */ - public function getAll($type, $count = 20) + public function getAll($type, $count = 20, $permission = 'view') { - $q = $this->entityQuery($type)->orderBy('name', 'asc'); - if ($count !== false) $q = $q->take($count); + $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc'); + if ($count !== false) { + $q = $q->take($count); + } return $q->get(); } @@ -231,7 +240,9 @@ class EntityRepo { $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)) ->orderBy('created_at', 'desc'); - if (strtolower($type) === 'page') $query = $query->where('draft', '=', false); + if (strtolower($type) === 'page') { + $query = $query->where('draft', '=', false); + } if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); } @@ -250,7 +261,9 @@ class EntityRepo { $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)) ->orderBy('updated_at', 'desc'); - if (strtolower($type) === 'page') $query = $query->where('draft', '=', false); + if (strtolower($type) === 'page') { + $query = $query->where('draft', '=', false); + } if ($additionalQuery !== false && is_callable($additionalQuery)) { $additionalQuery($query); } @@ -347,12 +360,16 @@ class EntityRepo $parents[$key] = $entities[$index]; $parents[$key]->setAttribute('pages', collect()); } - if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index]; + if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') { + $tree[] = $entities[$index]; + } $entities[$index]->book = $book; } foreach ($entities as $entity) { - if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue; + if ($entity->chapter_id === 0 || $entity->chapter_id === '0') { + continue; + } $parentKey = 'BookStack\\Chapter:' . $entity->chapter_id; if (!isset($parents[$parentKey])) { $tree[] = $entity; @@ -431,7 +448,9 @@ class EntityRepo if (strtolower($type) === 'page' || strtolower($type) === 'chapter') { $query = $query->where('book_id', '=', $bookId); } - if ($currentId) $query = $query->where('id', '!=', $currentId); + if ($currentId) { + $query = $query->where('id', '!=', $currentId); + } return $query->count() > 0; } @@ -558,7 +577,9 @@ class EntityRepo $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name)); $slug = preg_replace('/\s{2,}/', ' ', $slug); $slug = str_replace(' ', '-', $slug); - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5); + if ($slug === "") { + $slug = substr(md5(rand(1, 500)), 0, 5); + } return $slug; } @@ -599,7 +620,9 @@ class EntityRepo public function savePageRevision(Page $page, $summary = null) { $revision = $this->pageRevision->newInstance($page->toArray()); - if (setting('app-editor') !== 'markdown') $revision->markdown = ''; + if (setting('app-editor') !== 'markdown') { + $revision->markdown = ''; + } $revision->page_id = $page->id; $revision->slug = $page->slug; $revision->book_slug = $page->book->slug; @@ -627,7 +650,9 @@ class EntityRepo */ protected function formatHtml($htmlText) { - if ($htmlText == '') return $htmlText; + if ($htmlText == '') { + return $htmlText; + } libxml_use_internal_errors(true); $doc = new DOMDocument(); $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8')); @@ -641,7 +666,9 @@ class EntityRepo foreach ($childNodes as $index => $childNode) { /** @var \DOMElement $childNode */ - if (get_class($childNode) !== 'DOMElement') continue; + if (get_class($childNode) !== 'DOMElement') { + continue; + } // Overwrite id if not a BookStack custom id if ($childNode->hasAttribute('id')) { @@ -688,12 +715,17 @@ class EntityRepo $content = $page->html; $matches = []; preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches); - if (count($matches[0]) === 0) return $content; + if (count($matches[0]) === 0) { + return $content; + } + $topLevelTags = ['table', 'ul', 'ol']; foreach ($matches[1] as $index => $includeId) { $splitInclude = explode('#', $includeId, 2); $pageId = intval($splitInclude[0]); - if (is_nan($pageId)) continue; + if (is_nan($pageId)) { + continue; + } $matchedPage = $this->getById('page', $pageId, false, $ignorePermissions); if ($matchedPage === null) { @@ -714,8 +746,13 @@ class EntityRepo continue; } $innerContent = ''; - foreach ($matchingElem->childNodes as $childNode) { - $innerContent .= $doc->saveHTML($childNode); + $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags); + if ($isTopLevel) { + $innerContent .= $doc->saveHTML($matchingElem); + } else { + foreach ($matchingElem->childNodes as $childNode) { + $innerContent .= $doc->saveHTML($childNode); + } } $content = str_replace($matches[0][$index], trim($innerContent), $content); } @@ -748,7 +785,9 @@ class EntityRepo $page->updated_by = user()->id; $page->draft = true; - if ($chapter) $page->chapter_id = $chapter->id; + if ($chapter) { + $page->chapter_id = $chapter->id; + } $book->pages()->save($page); $page = $this->page->find($page->id); @@ -779,14 +818,18 @@ class EntityRepo */ public function getPageNav($pageContent) { - if ($pageContent == '') return []; + if ($pageContent == '') { + return []; + } libxml_use_internal_errors(true); $doc = new DOMDocument(); $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8')); $xPath = new DOMXPath($doc); $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); - if (is_null($headers)) return []; + if (is_null($headers)) { + return []; + } $tree = collect([]); foreach ($headers as $header) { @@ -802,7 +845,7 @@ class EntityRepo // Normalise headers if only smaller headers have been used if (count($tree) > 0) { $minLevel = $tree->pluck('level')->min(); - $tree = $tree->map(function($header) use ($minLevel) { + $tree = $tree->map(function ($header) use ($minLevel) { $header['level'] -= ($minLevel - 2); return $header; }); @@ -838,7 +881,9 @@ class EntityRepo $page->fill($input); $page->html = $this->formatHtml($input['html']); $page->text = $this->pageToPlainText($page); - if (setting('app-editor') !== 'markdown') $page->markdown = ''; + if (setting('app-editor') !== 'markdown') { + $page->markdown = ''; + } $page->updated_by = $userId; $page->revision_count++; $page->save(); @@ -900,7 +945,9 @@ class EntityRepo public function getUserPageDraftMessage(PageRevision $draft) { $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]); - if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message; + if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) { + return $message; + } return $message . "\n" . trans('entities.pages_draft_edited_notification'); } @@ -996,7 +1043,9 @@ class EntityRepo } $draft->fill($data); - if (setting('app-editor') !== 'markdown') $draft->markdown = ''; + if (setting('app-editor') !== 'markdown') { + $draft->markdown = ''; + } $draft->save(); return $draft; @@ -1103,17 +1152,4 @@ class EntityRepo $page->delete(); } - } - - - - - - - - - - - - diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 8ddde7b0f..ec2fda1d3 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -1,12 +1,9 @@ image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%'); return $this->returnPaginated($images, $page, $pageSize); @@ -104,13 +101,13 @@ class ImageRepo /** * Get gallery images with a particular filter criteria such as * being within the current book or page. - * @param int $pagination - * @param int $pageSize * @param $filter * @param $pageId + * @param int $pageNum + * @param int $pageSize * @return array */ - public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId) + public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24) { $images = $this->image->where('type', '=', 'gallery'); @@ -123,7 +120,7 @@ class ImageRepo $images = $images->whereIn('uploaded_to', $validPageIds); } - return $this->returnPaginated($images, $pagination, $pageSize); + return $this->returnPaginated($images, $pageNum, $pageSize); } /** @@ -132,6 +129,8 @@ class ImageRepo * @param string $type * @param int $uploadedTo * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0) { @@ -140,11 +139,39 @@ class ImageRepo return $image; } + /** + * Save a drawing the the database; + * @param string $base64Uri + * @param int $uploadedTo + * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + */ + public function saveDrawing(string $base64Uri, int $uploadedTo) + { + $name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png'; + $image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo); + return $image; + } + + /** + * Replace the image content of a drawing. + * @param Image $image + * @param string $base64Uri + * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + */ + public function replaceDrawingContent(Image $image, string $base64Uri) + { + return $this->imageService->replaceImageDataFromBase64Uri($image, $base64Uri); + } + /** * Update the details of an image via an array of properties. * @param Image $image * @param array $updateDetails * @return Image + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ public function updateImageDetails(Image $image, $updateDetails) { @@ -170,6 +197,8 @@ class ImageRepo /** * Load thumbnails onto an image object. * @param Image $image + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ private function loadThumbs(Image $image) { @@ -183,22 +212,46 @@ class ImageRepo * Get the thumbnail for an image. * If $keepRatio is true only the width will be used. * Checks the cache then storage to avoid creating / accessing the filesystem on every check. - * * @param Image $image * @param int $width * @param int $height * @param bool $keepRatio * @return string + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \Exception */ public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { try { return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); - } catch (FileNotFoundException $exception) { - $image->delete(); - return []; + } catch (\Exception $exception) { + dd($exception); + return null; } } + /** + * Get the raw image data from an Image. + * @param Image $image + * @return null|string + */ + public function getImageData(Image $image) + { + try { + return $this->imageService->getImageData($image); + } catch (\Exception $exception) { + return null; + } + } -} \ No newline at end of file + /** + * Check if the provided image type is valid. + * @param $type + * @return bool + */ + public function isValidType($type) + { + $validTypes = ['drawing', 'gallery', 'cover', 'system', 'user']; + return in_array($type, $validTypes); + } +} diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index aa58d1718..6f7ea1dc8 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -1,6 +1,5 @@ permissionService->deleteJointPermissionsForRole($role); $role->delete(); } - -} \ No newline at end of file +} diff --git a/app/Repos/TagRepo.php b/app/Repos/TagRepo.php index 5edd6df3c..ab1805ab3 100644 --- a/app/Repos/TagRepo.php +++ b/app/Repos/TagRepo.php @@ -52,7 +52,9 @@ class TagRepo public function getForEntity($entityType, $entityId) { $entity = $this->getEntity($entityType, $entityId); - if ($entity === null) return collect(); + if ($entity === null) { + return collect(); + } return $entity->tags; } @@ -95,7 +97,9 @@ class TagRepo $query = $query->orderBy('count', 'desc')->take(50); } - if ($tagName !== false) $query = $query->where('name', '=', $tagName); + if ($tagName !== false) { + $query = $query->where('name', '=', $tagName); + } $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); return $query->get(['value'])->pluck('value'); @@ -112,7 +116,9 @@ class TagRepo $entity->tags()->delete(); $newTags = []; foreach ($tags as $tag) { - if (trim($tag['name']) === '') continue; + if (trim($tag['name']) === '') { + continue; + } $newTags[] = $this->newInstanceFromInput($tag); } @@ -132,5 +138,4 @@ class TagRepo $values = ['name' => $name, 'value' => $value]; return $this->tag->newInstance($values); } - -} \ No newline at end of file +} diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index c3546a442..3cfd61d27 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -1,8 +1,12 @@ user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']); if ($sortData['search']) { $term = '%' . $sortData['search'] . '%'; - $query->where(function($query) use ($term) { + $query->where(function ($query) use ($term) { $query->where('name', 'like', $term) ->orWhere('email', 'like', $term); }); @@ -83,16 +87,7 @@ class UserRepo $this->attachDefaultRole($user); // Get avatar from gravatar and save - if (!config('services.disable_services')) { - try { - $avatar = \Images::saveUserGravatar($user); - $user->avatar()->associate($avatar); - $user->save(); - } catch (Exception $e) { - $user->save(); - \Log::error('Failed to save user gravatar image'); - } - } + $this->downloadGravatarToUserAvatar($user); return $user; } @@ -104,10 +99,27 @@ class UserRepo public function attachDefaultRole($user) { $roleId = setting('registration-role'); - if ($roleId === false) $roleId = $this->role->first()->id; + if ($roleId === false) { + $roleId = $this->role->first()->id; + } $user->attachRoleId($roleId); } + /** + * Assign a user to a system-level role. + * @param User $user + * @param $systemRoleName + * @throws NotFoundException + */ + public function attachSystemRole(User $user, $systemRoleName) + { + $role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first(); + if ($role === null) { + throw new NotFoundException("Role '{$systemRoleName}' not found"); + } + $user->attachRole($role); + } + /** * Checks if the give user is the only admin. * @param User $user @@ -115,10 +127,14 @@ class UserRepo */ public function isOnlyAdmin(User $user) { - if (!$user->roles->pluck('name')->contains('admin')) return false; + if (!$user->hasSystemRole('admin')) { + return false; + } - $adminRole = $this->role->getRole('admin'); - if ($adminRole->users->count() > 1) return false; + $adminRole = $this->role->getSystemRole('admin'); + if ($adminRole->users->count() > 1) { + return false; + } return true; } @@ -140,11 +156,18 @@ class UserRepo /** * Remove the given user from storage, Delete all related content. * @param User $user + * @throws Exception */ public function destroy(User $user) { $user->socialAccounts()->delete(); $user->delete(); + + // Delete user profile images + $profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get(); + foreach ($profileImages as $image) { + Images::destroyImage($image); + } } /** @@ -156,7 +179,7 @@ class UserRepo */ public function getActivity(User $user, $count = 20, $page = 0) { - return \Activity::userActivity($user, $count, $page); + return Activity::userActivity($user, $count, $page); } /** @@ -213,4 +236,27 @@ class UserRepo return $this->role->where('system_name', '!=', 'admin')->get(); } -} \ No newline at end of file + /** + * Get a gravatar image for a user and set it as their avatar. + * Does not run if gravatar disabled in config. + * @param User $user + * @return bool + */ + public function downloadGravatarToUserAvatar(User $user) + { + // Get avatar from gravatar and save + if (!config('services.gravatar')) { + return false; + } + + try { + $avatar = Images::saveUserGravatar($user); + $user->avatar()->associate($avatar); + $user->save(); + return true; + } catch (Exception $e) { + \Log::error('Failed to save user gravatar image'); + return false; + } + } +} diff --git a/app/Role.php b/app/Role.php index bf9685ee2..e86854e79 100644 --- a/app/Role.php +++ b/app/Role.php @@ -1,6 +1,5 @@ getRelationValue('permissions'); foreach ($permissions as $permission) { - if ($permission->getRawAttribute('name') === $permissionName) return true; + if ($permission->getRawAttribute('name') === $permissionName) { + return true; + } } return false; } @@ -91,5 +92,4 @@ class Role extends Model { return static::where('hidden', '=', false)->orderBy('name')->get(); } - } diff --git a/app/RolePermission.php b/app/RolePermission.php index ded6f6394..366c16749 100644 --- a/app/RolePermission.php +++ b/app/RolePermission.php @@ -1,6 +1,5 @@ belongsToMany(Role::class, 'permission_role','permission_id', 'role_id'); + return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id'); } /** diff --git a/app/SearchTerm.php b/app/SearchTerm.php index 50df34021..ee6c72190 100644 --- a/app/SearchTerm.php +++ b/app/SearchTerm.php @@ -14,5 +14,4 @@ class SearchTerm extends Model { return $this->morphTo('entity'); } - } diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index 2368ba10a..3fc7e7ee0 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -170,5 +170,4 @@ class ActivityService Session::flash('success', $message); } } - -} \ No newline at end of file +} diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php index 592d67e63..381e44749 100644 --- a/app/Services/AttachmentService.php +++ b/app/Services/AttachmentService.php @@ -8,15 +8,37 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; class AttachmentService extends UploadService { + /** + * Get the storage that will be used for storing files. + * @return \Illuminate\Contracts\Filesystem\Filesystem + */ + protected function getStorage() + { + if ($this->storageInstance !== null) { + return $this->storageInstance; + } + + $storageType = config('filesystems.default'); + + // Override default location if set to local public to ensure not visible. + if ($storageType === 'local') { + $storageType = 'local_secure'; + } + + $this->storageInstance = $this->fileSystem->disk($storageType); + + return $this->storageInstance; + } + /** * Get an attachment from storage. * @param Attachment $attachment * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException */ public function getAttachmentFromStorage(Attachment $attachment) { - $attachmentPath = $this->getStorageBasePath() . $attachment->path; - return $this->getStorage()->get($attachmentPath); + return $this->getStorage()->get($attachment->path); } /** @@ -92,16 +114,6 @@ class AttachmentService extends UploadService ]); } - /** - * Get the file storage base path, amended for storage type. - * This allows us to keep a generic path in the database. - * @return string - */ - private function getStorageBasePath() - { - return $this->isLocal() ? 'storage/' : ''; - } - /** * Updates the file ordering for a listing of attached files. * @param array $attachmentList @@ -138,6 +150,7 @@ class AttachmentService extends UploadService /** * Delete a File from the database and storage. * @param Attachment $attachment + * @throws Exception */ public function deleteFile(Attachment $attachment) { @@ -157,11 +170,10 @@ class AttachmentService extends UploadService */ protected function deleteFileInStorage(Attachment $attachment) { - $storedFilePath = $this->getStorageBasePath() . $attachment->path; $storage = $this->getStorage(); - $dirPath = dirname($storedFilePath); + $dirPath = dirname($attachment->path); - $storage->delete($storedFilePath); + $storage->delete($attachment->path); if (count($storage->allFiles($dirPath)) === 0) { $storage->deleteDirectory($dirPath); } @@ -179,23 +191,20 @@ class AttachmentService extends UploadService $attachmentData = file_get_contents($uploadedFile->getRealPath()); $storage = $this->getStorage(); - $attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/'; - $storageBasePath = $this->getStorageBasePath() . $attachmentBasePath; + $basePath = 'uploads/files/' . Date('Y-m-M') . '/'; $uploadFileName = $attachmentName; - while ($storage->exists($storageBasePath . $uploadFileName)) { + while ($storage->exists($basePath . $uploadFileName)) { $uploadFileName = str_random(3) . $uploadFileName; } - $attachmentPath = $attachmentBasePath . $uploadFileName; - $attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath; - + $attachmentPath = $basePath . $uploadFileName; try { - $storage->put($attachmentStoragePath, $attachmentData); + $storage->put($attachmentPath, $attachmentData); } catch (Exception $e) { - throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath])); + throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath])); } + return $attachmentPath; } - -} \ No newline at end of file +} diff --git a/app/Services/EmailConfirmationService.php b/app/Services/EmailConfirmationService.php index 8eb52708c..9ee69ef1a 100644 --- a/app/Services/EmailConfirmationService.php +++ b/app/Services/EmailConfirmationService.php @@ -108,6 +108,4 @@ class EmailConfirmationService } return $token; } - - -} \ No newline at end of file +} diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 1e4e99428..ada2261e4 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -42,7 +42,7 @@ class ExportService public function chapterToContainedHtml(Chapter $chapter) { $pages = $this->entityRepo->getChapterChildren($chapter); - $pages->each(function($page) { + $pages->each(function ($page) { $page->html = $this->entityRepo->renderPage($page); }); $html = view('chapters/export', [ @@ -89,7 +89,7 @@ class ExportService public function chapterToPdf(Chapter $chapter) { $pages = $this->entityRepo->getChapterChildren($chapter); - $pages->each(function($page) { + $pages->each(function ($page) { $page->html = $this->entityRepo->renderPage($page); }); $html = view('chapters/export', [ @@ -163,7 +163,9 @@ class ExportService $pathString = public_path(trim($relString, '/')); } - if ($isLocal && !file_exists($pathString)) continue; + if ($isLocal && !file_exists($pathString)) { + continue; + } try { if ($isLocal) { $imageContent = file_get_contents($pathString); @@ -173,7 +175,9 @@ class ExportService $imageContent = curl_exec($ch); $err = curl_error($ch); curl_close($ch); - if ($err) throw new \Exception("Image fetch failed, Received error: " . $err); + if ($err) { + throw new \Exception("Image fetch failed, Received error: " . $err); + } } $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); @@ -257,17 +261,4 @@ class ExportService } return $text; } - } - - - - - - - - - - - - diff --git a/app/Services/Facades/Activity.php b/app/Services/Facades/Activity.php index ea04cfc47..d24e39dba 100644 --- a/app/Services/Facades/Activity.php +++ b/app/Services/Facades/Activity.php @@ -1,6 +1,5 @@ saveNew($imageName, $imageData, $type, $uploadedTo); } + /** + * Save a new image from a uri-encoded base64 string of data. + * @param string $base64Uri + * @param string $name + * @param string $type + * @param int $uploadedTo + * @return Image + * @throws ImageUploadException + */ + public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0) + { + $splitData = explode(';base64,', $base64Uri); + if (count($splitData) < 2) { + throw new ImageUploadException("Invalid base64 image data provided"); + } + $data = base64_decode($splitData[1]); + return $this->saveNew($name, $data, $type, $uploadedTo); + } + + /** + * Replace the data for an image via a Base64 encoded string. + * @param Image $image + * @param string $base64Uri + * @return Image + * @throws ImageUploadException + */ + public function replaceImageDataFromBase64Uri(Image $image, string $base64Uri) + { + $splitData = explode(';base64,', $base64Uri); + if (count($splitData) < 2) { + throw new ImageUploadException("Invalid base64 image data provided"); + } + $data = base64_decode($splitData[1]); + $storage = $this->getStorage(); + + try { + $storage->put($image->path, $data); + } catch (Exception $e) { + throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $image->path])); + } + + return $image; + } /** * Gets an image from url and saves it to the database. @@ -59,7 +102,9 @@ class ImageService extends UploadService { $imageName = $imageName ? $imageName : basename($url); $imageData = file_get_contents($url); - if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); + if ($imageData === false) { + throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); + } return $this->saveNew($imageName, $imageData, $type); } @@ -78,12 +123,12 @@ class ImageService extends UploadService $secureUploads = setting('app-secure-images'); $imageName = str_replace(' ', '-', $imageName); - if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; + if ($secureUploads) { + $imageName = str_random(16) . '-' . $imageName; + } $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; - if ($this->isLocal()) $imagePath = '/public' . $imagePath; - while ($storage->exists($imagePath . $imageName)) { $imageName = str_random(3) . $imageName; } @@ -96,8 +141,6 @@ class ImageService extends UploadService throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath])); } - if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath); - $imageDetails = [ 'name' => $imageName, 'path' => $fullPath, @@ -112,8 +155,8 @@ class ImageService extends UploadService $imageDetails['updated_by'] = $userId; } - $image = Image::forceCreate($imageDetails); - + $image = (new Image()); + $image->forceFill($imageDetails)->save(); return $image; } @@ -124,14 +167,13 @@ class ImageService extends UploadService */ protected function getPath(Image $image) { - return ($this->isLocal()) ? ('public/' . $image->path) : $image->path; + return $image->path; } /** * Get the thumbnail for an image. * If $keepRatio is true only the width will be used. * Checks the cache then storage to avoid creating / accessing the filesystem on every check. - * * @param Image $image * @param int $width * @param int $height @@ -151,7 +193,6 @@ class ImageService extends UploadService } $storage = $this->getStorage(); - if ($storage->exists($thumbFilePath)) { return $this->getPublicUrl($thumbFilePath); } @@ -161,9 +202,8 @@ class ImageService extends UploadService } catch (Exception $e) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { throw new ImageUploadException(trans('errors.cannot_create_thumbs')); - } else { - throw $e; } + throw $e; } if ($keepRatio) { @@ -183,10 +223,24 @@ class ImageService extends UploadService return $this->getPublicUrl($thumbFilePath); } + /** + * Get the raw data content from an image. + * @param Image $image + * @return string + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function getImageData(Image $image) + { + $imagePath = $this->getPath($image); + $storage = $this->getStorage(); + return $storage->get($imagePath); + } + /** * Destroys an Image object along with its files and thumbnails. * @param Image $image * @return bool + * @throws Exception */ public function destroyImage(Image $image) { @@ -205,9 +259,13 @@ class ImageService extends UploadService // Cleanup of empty folders foreach ($storage->directories($imageFolder) as $directory) { - if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory); + if ($this->isFolderEmpty($directory)) { + $storage->deleteDirectory($directory); + } + } + if ($this->isFolderEmpty($imageFolder)) { + $storage->deleteDirectory($imageFolder); } - if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder); $image->delete(); return true; @@ -252,14 +310,10 @@ class ImageService extends UploadService $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; } } - $this->storageUrl = $storageUrl; } - if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath); - - return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath; + $basePath = ($this->storageUrl == false) ? baseUrl('/') : $this->storageUrl; + return rtrim($basePath, '/') . $filePath; } - - } diff --git a/app/Services/Ldap.php b/app/Services/Ldap.php index 9c3bec327..29270daf5 100644 --- a/app/Services/Ldap.php +++ b/app/Services/Ldap.php @@ -1,6 +1,5 @@ config['follow_referrals'] ? 1 : 0; $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]); - if ($users['count'] === 0) return null; + if ($users['count'] === 0) { + return null; + } $user = $users[0]; return [ @@ -66,8 +67,12 @@ class LdapService public function validateUserCredentials(Authenticatable $user, $username, $password) { $ldapUser = $this->getUserDetails($username); - if ($ldapUser === null) return false; - if ($ldapUser['uid'] !== $user->external_auth_id) return false; + if ($ldapUser === null) { + return false; + } + if ($ldapUser['uid'] !== $user->external_auth_id) { + return false; + } $ldapConnection = $this->getConnection(); try { @@ -97,7 +102,9 @@ class LdapService $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); } - if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); + if (!$ldapBind) { + throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); + } } /** @@ -108,7 +115,9 @@ class LdapService */ protected function getConnection() { - if ($this->ldapConnection !== null) return $this->ldapConnection; + if ($this->ldapConnection !== null) { + return $this->ldapConnection; + } // Check LDAP extension in installed if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { @@ -118,7 +127,9 @@ class LdapService // Get port from server string and protocol if specified. $ldapServer = explode(':', $this->config['server']); $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1; - if (!$hasProtocol) array_unshift($ldapServer, ''); + if (!$hasProtocol) { + array_unshift($ldapServer, ''); + } $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1]; $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389; $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort); @@ -151,5 +162,4 @@ class LdapService } return strtr($filterString, $newAttrs); } - -} \ No newline at end of file +} diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index 4f8b4e0f9..331ed06c8 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -88,7 +88,9 @@ class PermissionService } $book = $this->book->find($bookId); - if ($book === null) $book = false; + if ($book === null) { + $book = false; + } if (isset($this->entityCache['books'])) { $this->entityCache['books']->put($bookId, $book); } @@ -108,7 +110,9 @@ class PermissionService } $chapter = $this->chapter->find($chapterId); - if ($chapter === null) $chapter = false; + if ($chapter === null) { + $chapter = false; + } if (isset($this->entityCache['chapters'])) { $this->entityCache['chapters']->put($chapterId, $chapter); } @@ -122,7 +126,9 @@ class PermissionService */ protected function getRoles() { - if ($this->userRoles !== false) return $this->userRoles; + if ($this->userRoles !== false) { + return $this->userRoles; + } $roles = []; @@ -161,9 +167,9 @@ class PermissionService */ protected function bookFetchQuery() { - return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) { + return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { $query->select(['id', 'restricted', 'created_by', 'book_id']); - }, 'pages' => function($query) { + }, 'pages' => function ($query) { $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']); }]); } @@ -174,7 +180,8 @@ class PermissionService * @param array $roles * @param bool $deleteOld */ - protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) { + protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) + { $entities = clone $books; /** @var Book $book */ @@ -187,7 +194,9 @@ class PermissionService } } - if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all()); + if ($deleteOld) { + $this->deleteManyJointPermissionsForEntities($entities->all()); + } $this->createManyJointPermissions($entities, $roles); } @@ -261,7 +270,7 @@ class PermissionService */ protected function deleteManyJointPermissionsForRoles($roles) { - $roleIds = array_map(function($role) { + $roleIds = array_map(function ($role) { return $role->id; }, $roles); $this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete(); @@ -282,21 +291,22 @@ class PermissionService */ protected function deleteManyJointPermissionsForEntities($entities) { - if (count($entities) === 0) return; + if (count($entities) === 0) { + return; + } - $this->db->transaction(function() use ($entities) { + $this->db->transaction(function () use ($entities) { foreach (array_chunk($entities, 1000) as $entityChunk) { $query = $this->db->table('joint_permissions'); foreach ($entityChunk as $entity) { - $query->orWhere(function(QueryBuilder $query) use ($entity) { + $query->orWhere(function (QueryBuilder $query) use ($entity) { $query->where('entity_id', '=', $entity->id) ->where('entity_type', '=', $entity->getMorphClass()); }); } $query->delete(); } - }); } @@ -315,7 +325,7 @@ class PermissionService $permissionFetch = $this->entityPermission->newQuery(); foreach ($entities as $entity) { $entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted')); - $permissionFetch->orWhere(function($query) use ($entity) { + $permissionFetch->orWhere(function ($query) use ($entity) { $query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass()); }); } @@ -346,7 +356,7 @@ class PermissionService } } - $this->db->transaction(function() use ($jointPermissions) { + $this->db->transaction(function () use ($jointPermissions) { foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) { $this->db->table('joint_permissions')->insert($jointPermissionChunk); } @@ -362,8 +372,12 @@ class PermissionService protected function getActions(Entity $entity) { $baseActions = ['view', 'update', 'delete']; - if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create'; - if ($entity->isA('book')) $baseActions[] = 'chapter-create'; + if ($entity->isA('chapter') || $entity->isA('book')) { + $baseActions[] = 'page-create'; + } + if ($entity->isA('book')) { + $baseActions[] = 'chapter-create'; + } return $baseActions; } @@ -412,7 +426,10 @@ class PermissionService } } - return $this->createJointPermissionDataArray($entity, $role, $action, + return $this->createJointPermissionDataArray( + $entity, + $role, + $action, ($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)), ($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents)) ); @@ -426,7 +443,8 @@ class PermissionService * @param $action * @return bool */ - protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) { + protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) + { $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action; return isset($entityMap[$key]) ? $entityMap[$key] : false; } @@ -545,11 +563,12 @@ class PermissionService * @param bool $fetchPageContent * @return QueryBuilder */ - public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { - $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) { + public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) + { + $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) { $query->where('draft', '=', 0); if (!$filterDrafts) { - $query->orWhere(function($query) { + $query->orWhere(function ($query) { $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id); }); } @@ -562,8 +581,8 @@ class PermissionService $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)') ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type') ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles()) - ->where(function($query) { - $query->where('jp.has_permission', '=', 1)->orWhere(function($query) { + ->where(function ($query) { + $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) { $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id); }); }); @@ -715,5 +734,4 @@ class PermissionService $this->userRoles = false; $this->isAdminUser = null; } - -} \ No newline at end of file +} diff --git a/app/Services/SearchService.php b/app/Services/SearchService.php index a8784ded1..6786c5cf4 100644 --- a/app/Services/SearchService.php +++ b/app/Services/SearchService.php @@ -83,7 +83,9 @@ class SearchService $total = 0; foreach ($entityTypesToSearch as $entityType) { - if (!in_array($entityType, $entityTypes)) continue; + if (!in_array($entityType, $entityTypes)) { + continue; + } $search = $this->searchEntityTable($terms, $entityType, $page, $count); $total += $this->searchEntityTable($terms, $entityType, $page, $count, true); $results = $results->merge($search); @@ -111,7 +113,9 @@ class SearchService $results = collect(); foreach ($entityTypesToSearch as $entityType) { - if (!in_array($entityType, $entityTypes)) continue; + if (!in_array($entityType, $entityTypes)) { + continue; + } $search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get(); $results = $results->merge($search); } @@ -143,7 +147,9 @@ class SearchService public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false) { $query = $this->buildEntitySearchQuery($terms, $entityType); - if ($getCount) return $query->count(); + if ($getCount) { + return $query->count(); + } $query = $query->skip(($page-1) * $count)->take($count); return $query->get(); @@ -164,12 +170,12 @@ class SearchService if (count($terms['search']) > 0) { $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); $subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType)); - $subQuery->where(function(Builder $query) use ($terms) { + $subQuery->where(function (Builder $query) use ($terms) { foreach ($terms['search'] as $inputTerm) { $query->orWhere('term', 'like', $inputTerm .'%'); } })->groupBy('entity_type', 'entity_id'); - $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) { + $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) { $join->on('id', '=', 'entity_id'); })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc'); $entitySelect->mergeBindings($subQuery); @@ -177,7 +183,7 @@ class SearchService // Handle exact term matching if (count($terms['exact']) > 0) { - $entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) { + $entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) { foreach ($terms['exact'] as $inputTerm) { $query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) { $query->where('name', 'like', '%'.$inputTerm .'%') @@ -195,7 +201,9 @@ class SearchService // Handle filters foreach ($terms['filters'] as $filterTerm => $filterValue) { $functionName = camel_case('filter_' . $filterTerm); - if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue); + if (method_exists($this, $functionName)) { + $this->$functionName($entitySelect, $entity, $filterValue); + } } return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view'); @@ -234,7 +242,9 @@ class SearchService // Parse standard terms foreach (explode(' ', trim($searchString)) as $searchTerm) { - if ($searchTerm !== '') $terms['search'][] = $searchTerm; + if ($searchTerm !== '') { + $terms['search'][] = $searchTerm; + } } // Split filter values out @@ -267,15 +277,18 @@ class SearchService * @param string $tagTerm * @return mixed */ - protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) { + protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) + { preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); - $query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) { + $query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) { $tagName = $tagSplit[1]; $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : ''; $validOperator = in_array($tagOperator, $this->queryOperators); if (!empty($tagOperator) && !empty($tagValue) && $validOperator) { - if (!empty($tagName)) $query->where('name', '=', $tagName); + if (!empty($tagName)) { + $query->where('name', '=', $tagName); + } if (is_numeric($tagValue) && $tagOperator !== 'like') { // We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will // search the value as a string which prevents being able to do number-based operations @@ -323,7 +336,8 @@ class SearchService * Index multiple Entities at once * @param Entity[] $entities */ - protected function indexEntities($entities) { + protected function indexEntities($entities) + { $terms = []; foreach ($entities as $entity) { $nameTerms = $this->generateTermArrayFromText($entity->name, 5); @@ -386,7 +400,9 @@ class SearchService $token = strtok($text, $splitChars); while ($token !== false) { - if (!isset($tokenMap[$token])) $tokenMap[$token] = 0; + if (!isset($tokenMap[$token])) { + $tokenMap[$token] = 0; + } $tokenMap[$token]++; $token = strtok($splitChars); } @@ -410,43 +426,63 @@ class SearchService protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - try { $date = date_create($input); - } catch (\Exception $e) {return;} + try { + $date = date_create($input); + } catch (\Exception $e) { + return; + } $query->where('updated_at', '>=', $date); } protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - try { $date = date_create($input); - } catch (\Exception $e) {return;} + try { + $date = date_create($input); + } catch (\Exception $e) { + return; + } $query->where('updated_at', '<', $date); } protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - try { $date = date_create($input); - } catch (\Exception $e) {return;} + try { + $date = date_create($input); + } catch (\Exception $e) { + return; + } $query->where('created_at', '>=', $date); } protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - try { $date = date_create($input); - } catch (\Exception $e) {return;} + try { + $date = date_create($input); + } catch (\Exception $e) { + return; + } $query->where('created_at', '<', $date); } protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - if (!is_numeric($input) && $input !== 'me') return; - if ($input === 'me') $input = user()->id; + if (!is_numeric($input) && $input !== 'me') { + return; + } + if ($input === 'me') { + $input = user()->id; + } $query->where('created_by', '=', $input); } protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - if (!is_numeric($input) && $input !== 'me') return; - if ($input === 'me') $input = user()->id; + if (!is_numeric($input) && $input !== 'me') { + return; + } + if ($input === 'me') { + $input = user()->id; + } $query->where('updated_by', '=', $input); } @@ -455,7 +491,10 @@ class SearchService $query->where('name', 'like', '%' .$input. '%'); } - protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);} + protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + { + $this->filterInName($query, $model, $input); + } protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { @@ -469,14 +508,14 @@ class SearchService protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - $query->whereHas('views', function($query) { + $query->whereHas('views', function ($query) { $query->where('user_id', '=', user()->id); }); } protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { - $query->whereDoesntHave('views', function($query) { + $query->whereDoesntHave('views', function ($query) { $query->where('user_id', '=', user()->id); }); } @@ -484,7 +523,9 @@ class SearchService protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) { $functionName = camel_case('sort_by_' . $input); - if (method_exists($this, $functionName)) $this->$functionName($query, $model); + if (method_exists($this, $functionName)) { + $this->$functionName($query, $model); + } } @@ -500,4 +541,4 @@ class SearchService $query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc'); } -} \ No newline at end of file +} diff --git a/app/Services/SettingService.php b/app/Services/SettingService.php index 18a7c0d1b..7ec3ef93a 100644 --- a/app/Services/SettingService.php +++ b/app/Services/SettingService.php @@ -40,8 +40,12 @@ class SettingService */ public function get($key, $default = false) { - if ($default === false) $default = config('setting-defaults.' . $key, false); - if (isset($this->localCache[$key])) return $this->localCache[$key]; + if ($default === false) { + $default = config('setting-defaults.' . $key, false); + } + if (isset($this->localCache[$key])) { + return $this->localCache[$key]; + } $value = $this->getValueFromStore($key, $default); $formatted = $this->formatValue($value, $default); @@ -72,12 +76,16 @@ class SettingService { // Check for an overriding value $overrideValue = $this->getOverrideValue($key); - if ($overrideValue !== null) return $overrideValue; + if ($overrideValue !== null) { + return $overrideValue; + } // Check the cache $cacheKey = $this->cachePrefix . $key; $cacheVal = $this->cache->get($cacheKey, null); - if ($cacheVal !== null) return $cacheVal; + if ($cacheVal !== null) { + return $cacheVal; + } // Check the database $settingObject = $this->getSettingObjectByKey($key); @@ -98,6 +106,9 @@ class SettingService { $cacheKey = $this->cachePrefix . $key; $this->cache->forget($cacheKey); + if (isset($this->localCache[$key])) { + unset($this->localCache[$key]); + } } /** @@ -109,11 +120,17 @@ class SettingService protected function formatValue($value, $default) { // Change string booleans to actual booleans - if ($value === 'true') $value = true; - if ($value === 'false') $value = false; + if ($value === 'true') { + $value = true; + } + if ($value === 'false') { + $value = false; + } // Set to default if empty - if ($value === '') $value = $default; + if ($value === '') { + $value = $default; + } return $value; } @@ -222,8 +239,9 @@ class SettingService */ protected function getOverrideValue($key) { - if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false; + if ($key === 'registration-enabled' && config('auth.method') === 'ldap') { + return false; + } return null; } - -} \ No newline at end of file +} diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php index d52464539..02361e59b 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Services/SocialAuthService.php @@ -16,7 +16,7 @@ class SocialAuthService protected $socialite; protected $socialAccount; - protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta']; + protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch']; /** * SocialAuthService constructor. @@ -150,8 +150,12 @@ class SocialAuthService { $driver = trim(strtolower($socialDriver)); - if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found')); - if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)])); + if (!in_array($driver, $this->validSocialDrivers)) { + abort(404, trans('errors.social_driver_not_found')); + } + if (!$this->checkDriverConfigured($driver)) { + throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)])); + } return $driver; } @@ -220,5 +224,4 @@ class SocialAuthService session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)])); return redirect(user()->getEditUrl()); } - -} \ No newline at end of file +} diff --git a/app/Services/UploadService.php b/app/Services/UploadService.php index 44d4bb4f7..cd0567559 100644 --- a/app/Services/UploadService.php +++ b/app/Services/UploadService.php @@ -32,7 +32,9 @@ class UploadService */ protected function getStorage() { - if ($this->storageInstance !== null) return $this->storageInstance; + if ($this->storageInstance !== null) { + return $this->storageInstance; + } $storageType = config('filesystems.default'); $this->storageInstance = $this->fileSystem->disk($storageType); @@ -40,7 +42,6 @@ class UploadService return $this->storageInstance; } - /** * Check whether or not a folder is empty. * @param $path @@ -61,4 +62,4 @@ class UploadService { return strtolower(config('filesystems.default')) === 'local'; } -} \ No newline at end of file +} diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index 770a9e39f..ddcf2eb7e 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -27,7 +27,9 @@ class ViewService public function add(Entity $entity) { $user = user(); - if ($user === null || $user->isDefault()) return 0; + if ($user === null || $user->isDefault()) { + return 0; + } $view = $entity->views()->where('user_id', '=', $user->id)->first(); // Add view if model exists if ($view) { @@ -77,12 +79,16 @@ class ViewService public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false) { $user = user(); - if ($user === null || $user->isDefault()) return collect(); + if ($user === null || $user->isDefault()) { + return collect(); + } $query = $this->permissionService ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); - if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); + if ($filterModel) { + $query = $query->where('viewable_type', '=', get_class($filterModel)); + } $query = $query->where('user_id', '=', $user->id); $viewables = $query->with('viewable')->orderBy('updated_at', 'desc') @@ -97,5 +103,4 @@ class ViewService { $this->view->truncate(); } - -} \ No newline at end of file +} diff --git a/app/SocialAccount.php b/app/SocialAccount.php index e7c9b4cc5..fdba6a04f 100644 --- a/app/SocialAccount.php +++ b/app/SocialAccount.php @@ -1,6 +1,5 @@ morphTo('entity'); } -} \ No newline at end of file +} diff --git a/app/User.php b/app/User.php index 8033557e4..d1e9b38a7 100644 --- a/app/User.php +++ b/app/User.php @@ -60,7 +60,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function roles() { - if ($this->id === 0) return ; + if ($this->id === 0) { + return ; + } return $this->belongsToMany(Role::class); } @@ -81,7 +83,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function hasSystemRole($role) { - return $this->roles->pluck('system_name')->contains('admin'); + return $this->roles->pluck('system_name')->contains($role); } /** @@ -91,9 +93,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function permissions($cache = true) { - if(isset($this->permissions) && $cache) return $this->permissions; + if (isset($this->permissions) && $cache) { + return $this->permissions; + } $this->load('roles.permissions'); - $permissions = $this->roles->map(function($role) { + $permissions = $this->roles->map(function ($role) { return $role->permissions; })->flatten()->unique(); $this->permissions = $permissions; @@ -107,7 +111,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function can($permissionName) { - if ($this->email === 'guest') return false; + if ($this->email === 'guest') { + return false; + } return $this->permissions()->pluck('name')->contains($permissionName); } @@ -162,7 +168,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon { $default = baseUrl('/user_avatar.png'); $imageId = $this->image_id; - if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default; + if ($imageId === 0 || $imageId === '0' || $imageId === null) { + return $default; + } try { $avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default; @@ -206,10 +214,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function getShortName($chars = 8) { - if (strlen($this->name) <= $chars) return $this->name; + if (strlen($this->name) <= $chars) { + return $this->name; + } $splitName = explode(' ', $this->name); - if (strlen($splitName[0]) <= $chars) return $splitName[0]; + if (strlen($splitName[0]) <= $chars) { + return $splitName[0]; + } return ''; } diff --git a/app/helpers.php b/app/helpers.php index 153f1e49f..daa747e71 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -74,7 +74,9 @@ function userCan($permission, Ownable $ownable = null) function setting($key = null, $default = false) { $settingService = resolve(\BookStack\Services\SettingService::class); - if (is_null($key)) return $settingService; + if (is_null($key)) { + return $settingService; + } return $settingService->get($key, $default); } @@ -87,7 +89,9 @@ function setting($key = null, $default = false) function baseUrl($path, $forceAppDomain = false) { $isFullUrl = strpos($path, 'http') === 0; - if ($isFullUrl && !$forceAppDomain) return $path; + if ($isFullUrl && !$forceAppDomain) { + return $path; + } $path = trim($path, '/'); // Remove non-specified domain if forced and we have a domain @@ -126,7 +130,8 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null) return app('redirect')->to($to, $status, $headers, $secure); } -function icon($name, $attrs = []) { +function icon($name, $attrs = []) +{ $iconPath = resource_path('assets/icons/' . $name . '.svg'); $attrString = ' '; foreach ($attrs as $attrName => $attr) { @@ -159,11 +164,15 @@ function sortUrl($path, $data, $overrideData = []) foreach ($queryData as $name => $value) { $trimmedVal = trim($value); - if ($trimmedVal === '') continue; + if ($trimmedVal === '') { + continue; + } $queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal); } - if (count($queryStringSections) === 0) return $path; + if (count($queryStringSections) === 0) { + return $path; + } return baseUrl($path . '?' . implode('&', $queryStringSections)); -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 5b9802f52..5106ed0bb 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,9 @@ "barryvdh/laravel-snappy": "^0.4.0", "socialiteproviders/slack": "^3.0", "socialiteproviders/microsoft-azure": "^3.0", - "socialiteproviders/okta": "^1.0" + "socialiteproviders/okta": "^1.0", + "socialiteproviders/gitlab": "^3.0", + "socialiteproviders/twitch": "^3.0" }, "require-dev": { "filp/whoops": "~2.0", @@ -29,7 +31,8 @@ "symfony/dom-crawler": "3.1.*", "laravel/browser-kit-testing": "^2.0", "barryvdh/laravel-ide-helper": "^2.4.1", - "barryvdh/laravel-debugbar": "^3.1.0" + "barryvdh/laravel-debugbar": "^3.1.0", + "squizlabs/php_codesniffer": "^3.2" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index 057e6007a..9370bebff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "7d60f09393b99551e9ffdb6622ed7ade", + "content-hash": "ed85d10e69b1071020178cb400a80e48", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.45.3", + "version": "3.52.6", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "d0abb0b1194fa64973b135191f56df991bc5787c" + "reference": "c9af7657eddc0267cc7ac4f969c10d5c18459992" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d0abb0b1194fa64973b135191f56df991bc5787c", - "reference": "d0abb0b1194fa64973b135191f56df991bc5787c", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c9af7657eddc0267cc7ac4f969c10d5c18459992", + "reference": "c9af7657eddc0267cc7ac4f969c10d5c18459992", "shasum": "" }, "require": { @@ -84,25 +84,25 @@ "s3", "sdk" ], - "time": "2017-12-08T21:36:50+00:00" + "time": "2018-02-09T22:53:37+00:00" }, { "name": "barryvdh/laravel-dompdf", - "version": "v0.8.1", + "version": "v0.8.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-dompdf.git", - "reference": "3b2235e589616331d68482d61b7763789a2600fe" + "reference": "7dcdecfa125c174d0abe723603633dc2756ea3af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/3b2235e589616331d68482d61b7763789a2600fe", - "reference": "3b2235e589616331d68482d61b7763789a2600fe", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7dcdecfa125c174d0abe723603633dc2756ea3af", + "reference": "7dcdecfa125c174d0abe723603633dc2756ea3af", "shasum": "" }, "require": { "dompdf/dompdf": "^0.8", - "illuminate/support": "5.1.x|5.2.x|5.3.x|5.4.x|5.5.x", + "illuminate/support": "5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x", "php": ">=5.5.9" }, "type": "library", @@ -140,32 +140,32 @@ "laravel", "pdf" ], - "time": "2017-07-29T19:01:17+00:00" + "time": "2018-02-07T17:43:25+00:00" }, { "name": "barryvdh/laravel-snappy", - "version": "v0.4.0", + "version": "v0.4.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-snappy.git", - "reference": "f08c7e5b4ddea585bfcd48ab4f40f920e58dd1cf" + "reference": "5f6e7f3ba15c867d1b8e2885d454110270616ebe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/f08c7e5b4ddea585bfcd48ab4f40f920e58dd1cf", - "reference": "f08c7e5b4ddea585bfcd48ab4f40f920e58dd1cf", + "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/5f6e7f3ba15c867d1b8e2885d454110270616ebe", + "reference": "5f6e7f3ba15c867d1b8e2885d454110270616ebe", "shasum": "" }, "require": { - "illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x", - "illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x", - "knplabs/knp-snappy": "*", + "illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x", + "illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x", + "knplabs/knp-snappy": "^1", "php": ">=5.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.3-dev" + "dev-master": "0.4-dev" }, "laravel": { "providers": [ @@ -201,7 +201,7 @@ "wkhtmltoimage", "wkhtmltopdf" ], - "time": "2017-08-14T06:48:50+00:00" + "time": "2018-02-08T15:58:26+00:00" }, { "name": "cogpowered/finediff", @@ -898,16 +898,16 @@ }, { "name": "knplabs/knp-snappy", - "version": "v1.0.3", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/KnpLabs/snappy.git", - "reference": "68590ef3aa94425b1c0019cc28ce471729f51fcb" + "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/68590ef3aa94425b1c0019cc28ce471729f51fcb", - "reference": "68590ef3aa94425b1c0019cc28ce471729f51fcb", + "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/144c4ecd1ccaeda936bf832b93079efc490e6850", + "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850", "shasum": "" }, "require": { @@ -960,20 +960,20 @@ "thumbnail", "wkhtmltopdf" ], - "time": "2017-12-03T23:18:18+00:00" + "time": "2018-01-22T19:40:51+00:00" }, { "name": "laravel/framework", - "version": "v5.5.24", + "version": "v5.5.34", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "06135405bb1f736dac5e9529ed1541fc446c9c0f" + "reference": "1de7c0aec13eadbdddc2d1ba4019b064b2c6b966" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/06135405bb1f736dac5e9529ed1541fc446c9c0f", - "reference": "06135405bb1f736dac5e9529ed1541fc446c9c0f", + "url": "https://api.github.com/repos/laravel/framework/zipball/1de7c0aec13eadbdddc2d1ba4019b064b2c6b966", + "reference": "1de7c0aec13eadbdddc2d1ba4019b064b2c6b966", "shasum": "" }, "require": { @@ -1053,6 +1053,7 @@ "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).", "laravel/tinker": "Required to use the tinker console command (~1.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", + "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).", "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", "nexmo/client": "Required to use the Nexmo transport (~1.0).", "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", @@ -1093,7 +1094,7 @@ "framework", "laravel" ], - "time": "2017-12-07T01:28:21+00:00" + "time": "2018-02-06T15:36:55+00:00" }, { "name": "laravel/socialite", @@ -1159,16 +1160,16 @@ }, { "name": "league/flysystem", - "version": "1.0.41", + "version": "1.0.42", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "f400aa98912c561ba625ea4065031b7a41e5a155" + "reference": "09eabc54e199950041aef258a85847676496fe8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f400aa98912c561ba625ea4065031b7a41e5a155", - "reference": "f400aa98912c561ba625ea4065031b7a41e5a155", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/09eabc54e199950041aef258a85847676496fe8e", + "reference": "09eabc54e199950041aef258a85847676496fe8e", "shasum": "" }, "require": { @@ -1179,12 +1180,13 @@ }, "require-dev": { "ext-fileinfo": "*", - "mockery/mockery": "~0.9", - "phpspec/phpspec": "^2.2", - "phpunit/phpunit": "~4.8" + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7" }, "suggest": { "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", @@ -1238,7 +1240,7 @@ "sftp", "storage" ], - "time": "2017-08-06T17:41:04+00:00" + "time": "2018-01-27T16:03:56+00:00" }, { "name": "league/flysystem-aws-s3-v3", @@ -1951,16 +1953,16 @@ }, { "name": "ramsey/uuid", - "version": "3.7.1", + "version": "3.7.3", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "45cffe822057a09e05f7bd09ec5fb88eeecd2334" + "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/45cffe822057a09e05f7bd09ec5fb88eeecd2334", - "reference": "45cffe822057a09e05f7bd09ec5fb88eeecd2334", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/44abcdad877d9a46685a3a4d221e3b2c4b87cb76", + "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76", "shasum": "" }, "require": { @@ -1971,17 +1973,15 @@ "rhumsaa/uuid": "self.version" }, "require-dev": { - "apigen/apigen": "^4.1", - "codeception/aspect-mock": "^1.0 | ^2.0", + "codeception/aspect-mock": "^1.0 | ~2.0.0", "doctrine/annotations": "~1.2.0", "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", "ircmaxell/random-lib": "^1.1", "jakub-onderka/php-parallel-lint": "^0.9.0", - "mockery/mockery": "^0.9.4", + "mockery/mockery": "^0.9.9", "moontoast/math": "^1.1", "php-mock/php-mock-phpunit": "^0.3|^1.1", - "phpunit/phpunit": "^4.7|>=5.0 <5.4", - "satooshi/php-coveralls": "^0.6.1", + "phpunit/phpunit": "^4.7|^5.0", "squizlabs/php_codesniffer": "^2.3" }, "suggest": { @@ -2029,7 +2029,7 @@ "identifier", "uuid" ], - "time": "2017-09-22T20:46:04+00:00" + "time": "2018-01-20T00:28:24+00:00" }, { "name": "sabberworm/php-css-parser", @@ -2076,17 +2076,54 @@ "time": "2016-07-19T19:14:21+00:00" }, { - "name": "socialiteproviders/manager", - "version": "v3.3.0", + "name": "socialiteproviders/gitlab", + "version": "v3.0.1", "source": { "type": "git", - "url": "https://github.com/SocialiteProviders/Manager.git", - "reference": "ac108bce073135a55dfebf28ceaf1459669348e8" + "url": "https://github.com/SocialiteProviders/GitLab.git", + "reference": "c96dc004563a3caf157608fe9aa9e45c79065d00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/ac108bce073135a55dfebf28ceaf1459669348e8", - "reference": "ac108bce073135a55dfebf28ceaf1459669348e8", + "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/c96dc004563a3caf157608fe9aa9e45c79065d00", + "reference": "c96dc004563a3caf157608fe9aa9e45c79065d00", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "socialiteproviders/manager": "~3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\GitLab\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christoffer Martinsen", + "email": "christoffermartinsen@gmail.com" + } + ], + "description": "GitLab OAuth2 Provider for Laravel Socialite", + "time": "2017-01-31T05:06:13+00:00" + }, + { + "name": "socialiteproviders/manager", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7", + "reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7", "shasum": "" }, "require": { @@ -2122,7 +2159,7 @@ } ], "description": "Easily add new or override built-in providers in Laravel Socialite.", - "time": "2017-09-21T07:21:55+00:00" + "time": "2017-11-20T08:42:57+00:00" }, { "name": "socialiteproviders/microsoft-azure", @@ -2235,6 +2272,43 @@ "description": "Slack OAuth2 Provider for Laravel Socialite", "time": "2017-04-10T05:10:48+00:00" }, + { + "name": "socialiteproviders/twitch", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Twitch.git", + "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/a7ad148c0b42d0c607d8a034b6e47faf5fc85e93", + "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "socialiteproviders/manager": "~3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Twitch\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Faust", + "email": "hello@brianfaust.de" + } + ], + "description": "Twitch OAuth2 Provider for Laravel Socialite", + "time": "2017-01-25T09:48:29+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.0.2", @@ -2721,16 +2795,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.6.0", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" + "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", - "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b", + "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b", "shasum": "" }, "require": { @@ -2742,7 +2816,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-master": "1.7-dev" } }, "autoload": { @@ -2776,7 +2850,7 @@ "portable", "shim" ], - "time": "2017-10-11T12:05:26+00:00" + "time": "2018-01-30T19:27:44+00:00" }, { "name": "symfony/process", @@ -3040,29 +3114,29 @@ }, { "name": "tijsverkoyen/css-to-inline-styles", - "version": "2.2.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b" + "reference": "0ed4a2ea4e0902dac0489e6436ebcd5bbcae9757" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", - "reference": "ab03919dfd85a74ae0372f8baf9f3c7d5c03b04b", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0ed4a2ea4e0902dac0489e6436ebcd5bbcae9757", + "reference": "0ed4a2ea4e0902dac0489e6436ebcd5bbcae9757", "shasum": "" }, "require": { - "php": "^5.5 || ^7", - "symfony/css-selector": "^2.7|~3.0" + "php": "^5.5 || ^7.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0" }, "require-dev": { - "phpunit/phpunit": "~4.8|5.1.*" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { @@ -3083,7 +3157,7 @@ ], "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", - "time": "2016-09-20T12:50:39+00:00" + "time": "2017-11-27T11:13:29+00:00" }, { "name": "vlucas/phpdotenv", @@ -3139,26 +3213,26 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.1.0", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "01a859752094e00aa8548832312366753272f8af" + "reference": "f0018d359a2ad6968ad11b283283a925e017f3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/01a859752094e00aa8548832312366753272f8af", - "reference": "01a859752094e00aa8548832312366753272f8af", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f0018d359a2ad6968ad11b283283a925e017f3c9", + "reference": "f0018d359a2ad6968ad11b283283a925e017f3c9", "shasum": "" }, "require": { - "illuminate/routing": "5.5.x", - "illuminate/session": "5.5.x", - "illuminate/support": "5.5.x", - "maximebf/debugbar": "~1.14.0", + "illuminate/routing": "5.5.x|5.6.x", + "illuminate/session": "5.5.x|5.6.x", + "illuminate/support": "5.5.x|5.6.x", + "maximebf/debugbar": "~1.15.0", "php": ">=7.0", - "symfony/debug": "^3", - "symfony/finder": "^3" + "symfony/debug": "^3|^4", + "symfony/finder": "^3|^4" }, "require-dev": { "illuminate/framework": "5.5.x" @@ -3166,7 +3240,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" }, "laravel": { "providers": [ @@ -3203,34 +3277,34 @@ "profiler", "webprofiler" ], - "time": "2017-09-18T13:32:46+00:00" + "time": "2018-02-07T08:29:09+00:00" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.4.1", + "version": "v2.4.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8" + "reference": "5c304db44fba8e9c4aa0c09739e59f7be7736fdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/2b1273c45e2f8df7a625563e2283a17c14f02ae8", - "reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/5c304db44fba8e9c4aa0c09739e59f7be7736fdd", + "reference": "5c304db44fba8e9c4aa0c09739e59f7be7736fdd", "shasum": "" }, "require": { "barryvdh/reflection-docblock": "^2.0.4", - "illuminate/console": "^5.0,<5.6", - "illuminate/filesystem": "^5.0,<5.6", - "illuminate/support": "^5.0,<5.6", + "illuminate/console": "^5.0,<5.7", + "illuminate/filesystem": "^5.0,<5.7", + "illuminate/support": "^5.0,<5.7", "php": ">=5.4.0", "symfony/class-loader": "^2.3|^3.0" }, "require-dev": { "doctrine/dbal": "~2.3", - "illuminate/config": "^5.0,<5.6", - "illuminate/view": "^5.0,<5.6", + "illuminate/config": "^5.0,<5.7", + "illuminate/view": "^5.0,<5.7", "phpunit/phpunit": "4.*", "scrutinizer/ocular": "~1.1", "squizlabs/php_codesniffer": "~2.3" @@ -3276,7 +3350,7 @@ "phpstorm", "sublime" ], - "time": "2017-07-16T00:24:12+00:00" + "time": "2018-02-08T07:56:07+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -3590,22 +3664,22 @@ }, { "name": "maximebf/debugbar", - "version": "v1.14.1", + "version": "v1.15.0", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2" + "reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/64251a392344e3d22f3d21c3b7c531ba96eb01d2", - "reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30e7d60937ee5f1320975ca9bc7bcdd44d500f07", + "reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07", "shasum": "" }, "require": { "php": ">=5.3.0", "psr/log": "^1.0", - "symfony/var-dumper": "^2.6|^3.0" + "symfony/var-dumper": "^2.6|^3.0|^4.0" }, "require-dev": { "phpunit/phpunit": "^4.0|^5.0" @@ -3647,7 +3721,7 @@ "debug", "debugbar" ], - "time": "2017-09-13T12:19:36+00:00" + "time": "2017-12-15T11:13:46+00:00" }, { "name": "mockery/mockery", @@ -3917,16 +3991,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.2.0", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "66465776cfc249844bde6d117abff1d22e06c2da" + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da", - "reference": "66465776cfc249844bde6d117abff1d22e06c2da", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", "shasum": "" }, "require": { @@ -3964,7 +4038,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-27T17:38:31+00:00" + "time": "2017-11-30T07:14:17+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4327,16 +4401,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.3", + "version": "6.5.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "882e886cc928a0abd3c61282b2a64026237d14a4" + "reference": "3330ef26ade05359d006041316ed0fa9e8e3cefe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/882e886cc928a0abd3c61282b2a64026237d14a4", - "reference": "882e886cc928a0abd3c61282b2a64026237d14a4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3330ef26ade05359d006041316ed0fa9e8e3cefe", + "reference": "3330ef26ade05359d006041316ed0fa9e8e3cefe", "shasum": "" }, "require": { @@ -4354,7 +4428,7 @@ "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.4", + "phpunit/phpunit-mock-objects": "^5.0.5", "sebastian/comparator": "^2.1", "sebastian/diff": "^2.0", "sebastian/environment": "^3.1", @@ -4407,27 +4481,27 @@ "testing", "xunit" ], - "time": "2017-12-06T09:42:03+00:00" + "time": "2018-02-01T05:57:37+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "5.0.4", + "version": "5.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "16b50f4167e5e85e81ca8a3dd105d0a5fd32009a" + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/16b50f4167e5e85e81ca8a3dd105d0a5fd32009a", - "reference": "16b50f4167e5e85e81ca8a3dd105d0a5fd32009a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.5", "php": "^7.0", "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.0" + "sebastian/exporter": "^3.1" }, "conflict": { "phpunit/phpunit": "<6.0" @@ -4466,7 +4540,7 @@ "mock", "xunit" ], - "time": "2017-12-02T05:31:19+00:00" + "time": "2018-01-06T05:45:45+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -4515,21 +4589,21 @@ }, { "name": "sebastian/comparator", - "version": "2.1.0", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "1174d9018191e93cb9d719edec01257fc05f8158" + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1174d9018191e93cb9d719edec01257fc05f8158", - "reference": "1174d9018191e93cb9d719edec01257fc05f8158", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", "shasum": "" }, "require": { "php": "^7.0", - "sebastian/diff": "^2.0", + "sebastian/diff": "^2.0 || ^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { @@ -4575,7 +4649,7 @@ "compare", "equality" ], - "time": "2017-11-03T07:16:52+00:00" + "time": "2018-02-01T13:46:46+00:00" }, { "name": "sebastian/diff", @@ -5027,6 +5101,57 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1", + "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2017-12-19T21:44:46+00:00" + }, { "name": "symfony/class-loader", "version": "v3.3.6", @@ -5181,16 +5306,16 @@ }, { "name": "webmozart/assert", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + "reference": "0df1908962e7a3071564e857d86874dad1ef204a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a", "shasum": "" }, "require": { @@ -5227,7 +5352,7 @@ "check", "validate" ], - "time": "2016-11-23T20:04:58+00:00" + "time": "2018-01-29T19:49:41+00:00" } ], "aliases": [], diff --git a/config/app.php b/config/app.php index 3be50b6c5..7e2e1487f 100755 --- a/config/app.php +++ b/config/app.php @@ -2,11 +2,14 @@ return [ - 'env' => env('APP_ENV', 'production'), 'editor' => env('APP_EDITOR', 'html'), + 'views' => [ + 'books' => env('APP_VIEWS_BOOKS', 'list') + ], + /* |-------------------------------------------------------------------------- | Application Debug Mode @@ -58,7 +61,7 @@ return [ */ 'locale' => env('APP_LANG', 'en'), - 'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it', 'ru'], + 'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN'], /* |-------------------------------------------------------------------------- diff --git a/config/filesystems.php b/config/filesystems.php index 836f68d3d..b7ebf5b2d 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -56,7 +56,12 @@ return [ 'local' => [ 'driver' => 'local', - 'root' => base_path(), + 'root' => public_path(), + ], + + 'local_secure' => [ + 'driver' => 'local', + 'root' => storage_path(), ], 'ftp' => [ diff --git a/config/services.php b/config/services.php index ba9be69de..825b1f109 100644 --- a/config/services.php +++ b/config/services.php @@ -13,7 +13,13 @@ return [ | to have a conventional place to find your various credentials. | */ + + // Single option to disable non-auth external services such as Gravatar and Draw.io 'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false), + 'gravatar' => env('GRAVATAR', !env('DISABLE_EXTERNAL_SERVICES', false)), + 'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)), + + 'callback_url' => env('APP_URL', false), 'mailgun' => [ @@ -86,7 +92,22 @@ return [ 'redirect' => env('APP_URL') . '/login/service/okta/callback', 'base_url' => env('OKTA_BASE_URL'), 'name' => 'Okta', - ], + ], + + 'gitlab' => [ + 'client_id' => env('GITLAB_APP_ID'), + 'client_secret' => env('GITLAB_APP_SECRET'), + 'redirect' => env('APP_URL') . '/login/service/gitlab/callback', + 'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances + 'name' => 'GitLab', + ], + + 'twitch' => [ + 'client_id' => env('TWITCH_APP_ID'), + 'client_secret' => env('TWITCH_APP_SECRET'), + 'redirect' => env('APP_URL') . '/login/service/twitch/callback', + 'name' => 'Twitch', + ], 'ldap' => [ 'server' => env('LDAP_SERVER', false), diff --git a/config/view.php b/config/view.php index e193ab61d..8dc2841e7 100644 --- a/config/view.php +++ b/config/view.php @@ -1,5 +1,10 @@ [ - realpath(base_path('resources/views')), - ], + 'paths' => $viewPaths, /* |-------------------------------------------------------------------------- diff --git a/package.json b/package.json index 23b01cf6e..42e892333 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "babelify": "^7.3.0", "browserify": "^14.3.0", "envify": "^4.0.0", - "gulp": "3.9.1", + "gulp": "^3.9.1", "gulp-autoprefixer": "3.1.1", "gulp-clean-css": "^3.0.4", "gulp-livereload": "^3.8.1", diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 000000000..009791fc9 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,8 @@ + + + The coding standard for BookStack. + app + */migrations/* + + + \ No newline at end of file diff --git a/public/system_images/drawing.svg b/public/system_images/drawing.svg new file mode 100644 index 000000000..9a9231a18 --- /dev/null +++ b/public/system_images/drawing.svg @@ -0,0 +1,107 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/readme.md b/readme.md index 1b3db4a56..97438d4bf 100644 --- a/readme.md +++ b/readme.md @@ -72,7 +72,17 @@ Some strings have colon-prefixed variables in such as `:userName`. Leave these v Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue. -Pull requests are very welcome. If the scope of your pull request is very large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge. +### Standards + +PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code. + +### Pull Requests + +Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge. + +Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. + +If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly. ## Website, Docs & Blog @@ -105,3 +115,4 @@ These are the great open-source projects used to help build BookStack: * [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy) * [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper) * [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html) +* [Draw.io](https://github.com/jgraph/drawio) diff --git a/resources/assets/icons/gitlab.svg b/resources/assets/icons/gitlab.svg new file mode 100644 index 000000000..fa1e92a56 --- /dev/null +++ b/resources/assets/icons/gitlab.svg @@ -0,0 +1 @@ + diff --git a/resources/assets/icons/twitch.svg b/resources/assets/icons/twitch.svg new file mode 100644 index 000000000..5dbc76bb9 --- /dev/null +++ b/resources/assets/icons/twitch.svg @@ -0,0 +1 @@ +Glitch \ No newline at end of file diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index 7b051dd12..3393829cc 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -1,6 +1,8 @@ const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); -const code = require('../code'); +const code = require('../libs/code'); + +const DrawIO = require('../libs/drawio'); class MarkdownEditor { @@ -20,13 +22,26 @@ class MarkdownEditor { init() { + let lastClick = 0; + // Prevent markdown display link click redirect this.display.addEventListener('click', event => { - let link = event.target.closest('a'); - if (link === null) return; + let isDblClick = Date.now() - lastClick < 300; - event.preventDefault(); - window.open(link.getAttribute('href')); + let link = event.target.closest('a'); + if (link !== null) { + event.preventDefault(); + window.open(link.getAttribute('href')); + return; + } + + let drawing = event.target.closest('[drawio-diagram]'); + if (drawing !== null && isDblClick) { + this.actionEditDrawing(drawing); + return; + } + + lastClick = Date.now(); }); // Button actions @@ -37,6 +52,7 @@ class MarkdownEditor { let action = button.getAttribute('data-action'); if (action === 'insertImage') this.actionInsertImage(); if (action === 'insertLink') this.actionShowLinkSelector(); + if (action === 'insertDrawing') this.actionStartDrawing(); }); window.$events.listen('editor-markdown-update', value => { @@ -290,6 +306,70 @@ class MarkdownEditor { }); } + // Show draw.io if enabled and handle save. + actionStartDrawing() { + if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return; + let cursorPos = this.cm.getCursor('from'); + + DrawIO.show(() => { + return Promise.resolve(''); + }, (pngData) => { + // let id = "image-" + Math.random().toString(16).slice(2); + // let loadingImage = window.baseUrl('/loading.gif'); + let data = { + image: pngData, + uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) + }; + + window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { + let newText = `
`; + this.cm.focus(); + this.cm.replaceSelection(newText); + this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length); + DrawIO.close(); + }).catch(err => { + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }); + } + + // Show draw.io if enabled and handle save. + actionEditDrawing(imgContainer) { + if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return; + let cursorPos = this.cm.getCursor('from'); + let drawingId = imgContainer.getAttribute('drawio-diagram'); + + DrawIO.show(() => { + return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { + return `data:image/png;base64,${resp.data.content}`; + }); + }, (pngData) => { + + let data = { + image: pngData, + uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) + }; + + window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => { + let newText = `
`; + let newContent = this.cm.getValue().split('\n').map(line => { + if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) { + return newText; + } + return line; + }).join('\n'); + this.cm.setValue(newContent); + this.cm.setCursor(cursorPos); + this.cm.focus(); + DrawIO.close(); + }).catch(err => { + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }); + } + } module.exports = MarkdownEditor ; \ No newline at end of file diff --git a/resources/assets/js/code.js b/resources/assets/js/libs/code.js similarity index 100% rename from resources/assets/js/code.js rename to resources/assets/js/libs/code.js diff --git a/resources/assets/js/libs/drawio.js b/resources/assets/js/libs/drawio.js new file mode 100644 index 000000000..beb6f0d59 --- /dev/null +++ b/resources/assets/js/libs/drawio.js @@ -0,0 +1,69 @@ + +const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json'; +let iFrame = null; + +let onInit, onSave; + +/** + * Show the draw.io editor. + * @param onInitCallback - Must return a promise with the xml to load for the editor. + * @param onSaveCallback - Is called with the drawing data on save. + */ +function show(onInitCallback, onSaveCallback) { + onInit = onInitCallback; + onSave = onSaveCallback; + + iFrame = document.createElement('iframe'); + iFrame.setAttribute('frameborder', '0'); + window.addEventListener('message', drawReceive); + iFrame.setAttribute('src', drawIoUrl); + iFrame.setAttribute('class', 'fullscreen'); + iFrame.style.backgroundColor = '#FFFFFF'; + document.body.appendChild(iFrame); +} + +function close() { + drawEventClose(); +} + +function drawReceive(event) { + if (!event.data || event.data.length < 1) return; + let message = JSON.parse(event.data); + if (message.event === 'init') { + drawEventInit(); + } else if (message.event === 'exit') { + drawEventClose(); + } else if (message.event === 'save') { + drawEventSave(message); + } else if (message.event === 'export') { + drawEventExport(message); + } +} + +function drawEventExport(message) { + if (onSave) { + onSave(message.data); + } +} + +function drawEventSave(message) { + drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'}); +} + +function drawEventInit() { + if (!onInit) return; + onInit().then(xml => { + drawPostMessage({action: 'load', autosave: 1, xml: xml}); + }); +} + +function drawEventClose() { + window.removeEventListener('message', drawReceive); + if (iFrame) document.body.removeChild(iFrame); +} + +function drawPostMessage(data) { + iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); +} + +module.exports = {show, close}; \ No newline at end of file diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 904403fc1..f7bfe22cf 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -1,5 +1,6 @@ "use strict"; -const Code = require('../code'); +const Code = require('../libs/code'); +const DrawIO = require('../libs/drawio'); /** * Handle pasting images from clipboard. @@ -47,7 +48,7 @@ function uploadImageFile(file) { let formData = new FormData(); formData.append('file', file, remoteFilename); - return window.$http.post('/images/gallery/upload', formData).then(resp => (resp.data)); + return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data)); } function registerEditorShortcuts(editor) { @@ -218,7 +219,103 @@ function codePlugin() { }); } -codePlugin(); + +function drawIoPlugin() { + + const drawIoUrl = 'https://www.draw.io/?embed=1&ui=atlas&spin=1&proto=json'; + let iframe = null; + let pageEditor = null; + let currentNode = null; + + function isDrawing(node) { + return node.hasAttribute('drawio-diagram'); + } + + function showDrawingEditor(mceEditor, selectedNode = null) { + pageEditor = mceEditor; + currentNode = selectedNode; + DrawIO.show(drawingInit, updateContent); + } + + function updateContent(pngData) { + let id = "image-" + Math.random().toString(16).slice(2); + let loadingImage = window.baseUrl('/loading.gif'); + let data = { + image: pngData, + uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) + }; + + // Handle updating an existing image + if (currentNode) { + DrawIO.close(); + let imgElem = currentNode.querySelector('img'); + let drawingId = currentNode.getAttribute('drawio-diagram'); + window.$http.put(window.baseUrl(`/images/drawing/upload/${drawingId}`), data).then(resp => { + pageEditor.dom.setAttrib(imgElem, 'src', `${resp.data.url}?updated=${Date.now()}`); + }).catch(err => { + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + return; + } + + setTimeout(() => { + pageEditor.insertContent(`
`); + DrawIO.close(); + window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { + pageEditor.dom.setAttrib(id, 'src', resp.data.url); + pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); + }).catch(err => { + pageEditor.dom.remove(id); + window.$events.emit('error', trans('errors.image_upload_error')); + console.log(err); + }); + }, 5); + } + + + function drawingInit() { + if (!currentNode) { + return Promise.resolve(''); + } + + let drawingId = currentNode.getAttribute('drawio-diagram'); + return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { + return `data:image/png;base64,${resp.data.content}`; + }); + } + + window.tinymce.PluginManager.add('drawio', function(editor, url) { + + editor.addCommand('drawio', () => { + showDrawingEditor(editor); + }); + + editor.addButton('drawio', { + tooltip: 'Drawing', + image: window.baseUrl('/system_images/drawing.svg'), + cmd: 'drawio' + }); + + editor.on('dblclick', event => { + let selectedNode = editor.selection.getNode(); + if (!isDrawing(selectedNode)) return; + showDrawingEditor(editor, selectedNode); + }); + + editor.on('SetContent', function () { + let drawings = editor.$('body > div[drawio-diagram]'); + if (!drawings.length) return; + + editor.undoManager.transact(function () { + drawings.each((index, elem) => { + elem.setAttribute('contenteditable', 'false'); + }); + }); + }); + + }); +} window.tinymce.PluginManager.add('customhr', function (editor) { editor.addCommand('InsertHorizontalRule', function () { @@ -242,7 +339,13 @@ window.tinymce.PluginManager.add('customhr', function (editor) { }); }); - +// Load plugins +let plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor"; +codePlugin(); +if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') === 'true') { + drawIoPlugin(); + plugins += ' drawio'; +} module.exports = { selector: '#html-editor', @@ -259,12 +362,12 @@ module.exports = { statusbar: false, menubar: false, paste_data_images: false, - extended_valid_elements: 'pre[*]', + extended_valid_elements: 'pre[*],svg[*],div[drawio-diagram]', automatic_uploads: false, - valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]", - plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor", + valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre],+div[img]", + plugins: plugins, imagetools_toolbar: 'imageoptions', - toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen", + toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr drawio | removeformat code fullscreen", content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", style_formats: [ {title: "Header Large", format: "h2"}, diff --git a/resources/assets/js/pages/page-show.js b/resources/assets/js/pages/page-show.js index 6af5af57d..2efaf66c6 100644 --- a/resources/assets/js/pages/page-show.js +++ b/resources/assets/js/pages/page-show.js @@ -1,5 +1,5 @@ const Clipboard = require("clipboard"); -const Code = require('../code'); +const Code = require('../libs/code'); let setupPageShow = window.setupPageShow = function (pageId) { diff --git a/resources/assets/js/vues/code-editor.js b/resources/assets/js/vues/code-editor.js index 35a98cc77..c7926cf28 100644 --- a/resources/assets/js/vues/code-editor.js +++ b/resources/assets/js/vues/code-editor.js @@ -1,4 +1,4 @@ -const codeLib = require('../code'); +const codeLib = require('../libs/code'); const methods = { show() { diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index 54e109067..5b72f5e1a 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -102,6 +102,10 @@ display: flex; align-self: flex-start; } + + .popup-content { + overflow-y: auto; + } } .corner-button { @@ -542,6 +546,15 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } } +@include smaller-than($m) { + #code-editor .lang-options { + max-width: 100%; + } + #code-editor .CodeMirror { + height: 200px; + } +} + .comment-box { border: 1px solid #DDD; margin-bottom: $-s; @@ -549,7 +562,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { .content { padding: $-s; font-size: 0.666em; - p, ul { + p, ul, ol { font-size: $fs-m; margin: .5em 0; } diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 457d30e54..69023f617 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -59,16 +59,22 @@ border: 1px solid #DDD; width: 50%; } - .markdown-display { - padding: 0 $-m 0; - margin-left: -1px; - overflow-y: scroll; - } - .markdown-display.page-content { +} + +.markdown-display { + padding: 0 $-m 0; + margin-left: -1px; + overflow-y: scroll; + &.page-content { margin: 0 auto; + width: 100%; max-width: 100%; } + [drawio-diagram]:hover { + outline: 2px solid $primary; + } } + .editor-toolbar { width: 100%; padding: $-xs $-m; diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss index c145f4280..880c619d1 100644 --- a/resources/assets/sass/_grid.scss +++ b/resources/assets/sass/_grid.scss @@ -175,6 +175,43 @@ div[class^="col-"] img { margin-right: -$-m; } +.grid { + display: grid; + grid-column-gap: $-l; + grid-row-gap: $-l; + &.third { + grid-template-columns: 1fr 1fr 1fr; + } +} + +.grid-card { + display: flex; + flex-direction: column; + border: 1px solid #ddd; + min-width: 100px; + .grid-card-content { + flex: 1; + } + .grid-card-content, .grid-card-footer { + padding: $-l; + } + .grid-card-content + .grid-card-footer { + padding-top: 0; + } +} + +@include smaller-than($m) { + .grid.third { + grid-template-columns: 1fr 1fr; + } +} + +@include smaller-than($s) { + .grid.third { + grid-template-columns: 1fr; + } +} + .float { float: left; &.right { @@ -195,14 +232,6 @@ div[class^="col-"] img { display: inline-block; } -@include larger-than(991px) { - .row.auto-clear .col-md-4:nth-child(3n+1){clear:left;} -} - -@include smaller-than(992px) { - .row.auto-clear .col-xs-6:nth-child(2n+1){clear:left;} -} - .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { position: relative; min-height: 1px; diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 0496d794e..5f06e1da9 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -382,13 +382,15 @@ ul.pagination { position: relative; overflow: hidden; background: #F2F2F2; - border: 1px solid #ddd; - border-bottom: 0; + a { + display: block; + } img { display: block; + width: 100%; max-width: 100%; height: auto; - transition: all .5s ease; + transition: all .5s ease-in-out; } img:hover { transform: scale(1.15); @@ -396,31 +398,31 @@ ul.pagination { } } -.book-grid-content { - padding: 30px; - border: 1px solid #ddd; +.book-grid-item .grid-card-content { border-top: 0; border-bottom-width: 2px; h2 { + width: 100%; font-size: 1.5em; margin: 0 0 10px; } h2 a { display: block; + width: 100%; line-height: 1.2; color: #009688;; text-decoration: none; } p { font-size: .85em; - margin: 0 0 10px; + margin: 0; line-height: 1.6em; } - p.small { - font-size: .8em; - } } -.book-grid-item { - margin-bottom : 20px; +.book-grid-item .grid-card-footer { + p.small { + font-size: .8em; + margin: 0; + } } diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 719126526..4309acc21 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -359,6 +359,11 @@ li.checkbox-item, li.task-list-item { color: inherit; } +.break-text { + white-space: pre-wrap; + word-wrap: break-word; +} + /** * Grouping */ diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss index 1f7caf1d9..bea516baa 100644 --- a/resources/assets/sass/export-styles.scss +++ b/resources/assets/sass/export-styles.scss @@ -13,4 +13,15 @@ table { border-spacing: 0; border-collapse: collapse; +} + +// Prevent code block overflow on export +pre { + padding-left: 12px; +} +pre:after { + display: none; +} +pre code { + white-space: pre-wrap; } \ No newline at end of file diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 6a80237c5..2cb72bd75 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -231,4 +231,16 @@ $btt-size: 40px; input { width: 100%; } +} + +.fullscreen { + border:0; + position:fixed; + top:0; + left:0; + right:0; + bottom:0; + width:100%; + height:100%; + z-index: 150; } \ No newline at end of file diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 7cdd7c23e..26f096327 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -49,6 +49,8 @@ return [ 'toggle_details' => 'Toggle Details', 'toggle_thumbnails' => 'Toggle Thumbnails', 'details' => 'Details', + 'grid_view' => 'Grid View', + 'list_view' => 'List View', /** * Header diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 4dc5ccc38..6c5dd9f77 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -162,6 +162,7 @@ return [ 'pages_md_preview' => 'Preview', 'pages_md_insert_image' => 'Insert Image', 'pages_md_insert_link' => 'Insert Entity Link', + 'pages_md_insert_drawing' => 'Insert Drawing', 'pages_not_in_chapter' => 'Page is not in a chapter', 'pages_move' => 'Move Page', 'pages_move_success' => 'Page moved to ":parentName"', diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index 18ed63c60..3b1d6e8b3 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -36,9 +36,11 @@ return [ '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', + 'image_upload_type_error' => 'The image type being uploaded is invalid', // Attachments 'attachment_page_mismatch' => 'Page mismatch during attachment update', + 'attachment_not_found' => 'Attachment not found', // Pages 'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index f35c486ad..d5ef4840e 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -96,7 +96,6 @@ return [ '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_books_view_type' => 'Preferred layout for books viewing', 'users_delete' => 'Delete User', 'users_delete_named' => 'Delete user :userName', 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', @@ -128,10 +127,12 @@ return [ 'nl' => 'Nederlands', 'pt_BR' => 'Português do Brasil', 'sk' => 'Slovensky', + 'sv' => 'Svenska', 'ja' => '日本語', 'pl' => 'Polski', 'it' => 'Italian', - 'ru' => 'Русский' + 'ru' => 'Русский', + 'zh_CN' => '简体中文' ] /////////////////////////////////// ]; diff --git a/resources/lang/sv/activities.php b/resources/lang/sv/activities.php new file mode 100644 index 000000000..2829bdd74 --- /dev/null +++ b/resources/lang/sv/activities.php @@ -0,0 +1,42 @@ + 'skapade sidan', + 'page_create_notification' => 'Sidan har skapats', + 'page_update' => 'uppdaterade sidan', + 'page_update_notification' => 'Sidan har uppdaterats', + 'page_delete' => 'tog bort sidan', + 'page_delete_notification' => 'Sidan har tagits bort', + 'page_restore' => 'återställde sidan', + 'page_restore_notification' => 'Sidan har återställts', + 'page_move' => 'flyttade sidan', + + // Chapters + 'chapter_create' => 'skapade kapitlet', + 'chapter_create_notification' => 'Kapitlet har skapats', + 'chapter_update' => 'uppdaterade kapitlet', + 'chapter_update_notification' => 'Kapitlet har uppdaterats', + 'chapter_delete' => 'tog bort kapitlet', + 'chapter_delete_notification' => 'Kapitlet har tagits bort', + 'chapter_move' => 'flyttade kapitlet', + + // Books + 'book_create' => 'skapade boken', + 'book_create_notification' => 'Boken har skapats', + 'book_update' => 'uppdaterade boken', + 'book_update_notification' => 'Boken har uppdaterats', + 'book_delete' => 'tog bort boken', + 'book_delete_notification' => 'Boken har tagits bort', + 'book_sort' => 'sorterade boken', + 'book_sort_notification' => 'Boken har sorterats om', + + // Other + 'commented_on' => 'kommenterade', +]; diff --git a/resources/lang/sv/auth.php b/resources/lang/sv/auth.php new file mode 100644 index 000000000..7382dadb1 --- /dev/null +++ b/resources/lang/sv/auth.php @@ -0,0 +1,76 @@ + 'Uppgifterna stämmer inte överrens med våra register.', + 'throttle' => 'För många inloggningsförsök. Prova igen om :seconds sekunder.', + + /** + * Login & Register + */ + 'sign_up' => 'Skapa konto', + 'log_in' => 'Logga in', + 'log_in_with' => 'Logga in med :socialDriver', + 'sign_up_with' => 'Registera dig med :socialDriver', + 'logout' => 'Logga ut', + + 'name' => 'Namn', + 'username' => 'Användarnamn', + 'email' => 'E-post', + 'password' => 'Lösenord', + 'password_confirm' => 'Bekräfta lösenord', + 'password_hint' => 'Måste vara fler än 5 tecken', + 'forgot_password' => 'Glömt lösenord?', + 'remember_me' => 'Kom ihåg mig', + 'ldap_email_hint' => 'Vänligen ange en e-postadress att använda till kontot.', + 'create_account' => 'Skapa konto', + 'social_login' => 'Logga in genom socialt medie', + 'social_registration' => 'Registrera dig genom socialt media', + 'social_registration_text' => 'Registrera dig och logga in genom en annan tjänst.', + + 'register_thanks' => 'Tack för din registrering!', + 'register_confirm' => 'Vänligen kontrollera din mail och klicka på bekräftelselänken för att få tillgång till :appName.', + 'registrations_disabled' => 'Registrering är för närvarande avstängd', + 'registration_email_domain_invalid' => 'Den e-postadressen har inte tillgång till den här applikationen', + 'register_success' => 'Tack för din registrering! Du är nu registerad och inloggad.', + + + /** + * Password Reset + */ + 'reset_password' => 'Återställ lösenord', + 'reset_password_send_instructions' => 'Ange din e-postadress nedan så skickar vi ett mail med en länk för att återställa ditt lösenord.', + 'reset_password_send_button' => 'Skicka återställningslänk', + 'reset_password_sent_success' => 'En länk för att återställa lösenordet har skickats till :email.', + 'reset_password_success' => 'Ditt lösenord har återställts.', + + 'email_reset_subject' => 'Återställ ditt lösenord till :appName', + 'email_reset_text' => 'Du får detta mail eftersom vi fått en begäran om att återställa lösenordet till ditt konto.', + 'email_reset_not_requested' => 'Om du inte begärt att få ditt lösenord återställt behöver du inte göra någonting', + + + /** + * Email Confirmation + */ + 'email_confirm_subject' => 'Bekräfta din e-post på :appName', + 'email_confirm_greeting' => 'Tack för att du gått med i :appName!', + 'email_confirm_text' => 'Vänligen bekräfta din e-postadress genom att klicka på knappen nedan:', + 'email_confirm_action' => 'Bekräfta e-post', + 'email_confirm_send_error' => 'E-posten behöver bekräftas men systemet kan inte skicka mail. Kontakta adminstratören för att kontrollera att allt är konfigurerat korrekt.', + 'email_confirm_success' => 'Din e-post har bekräftats', + 'email_confirm_resent' => 'Bekräftelsemailet har skickats på nytt, kolla din mail', + + 'email_not_confirmed' => 'E-posadress ej bekräftad', + 'email_not_confirmed_text' => 'Din e-postadress har inte bekräftats ännu.', + 'email_not_confirmed_click_link' => 'Vänligen klicka på länken i det mail du fick strax efter att du registerade dig.', + 'email_not_confirmed_resend' => 'Om du inte hittar mailet kan du begära en ny bekräftelse genom att fylla i formuläret nedan.', + 'email_not_confirmed_resend_button' => 'Skicka bekräftelse på nytt', +]; \ No newline at end of file diff --git a/resources/lang/sv/common.php b/resources/lang/sv/common.php new file mode 100644 index 000000000..e7c1e0648 --- /dev/null +++ b/resources/lang/sv/common.php @@ -0,0 +1,66 @@ + 'Avbryt', + 'confirm' => 'Bekräfta', + 'back' => 'Bakåt', + 'save' => 'Spara', + 'continue' => 'Fortsätt', + 'select' => 'Välj', + 'more' => 'Mer', + + /** + * Form Labels + */ + 'name' => 'Namn', + 'description' => 'Beskrivning', + 'role' => 'Roll', + 'cover_image' => 'Omslagsbild', + 'cover_image_description' => 'Bilden bör vara cirka 440x250px stor.', + + /** + * Actions + */ + 'actions' => 'Åtgärder', + 'view' => 'Visa', + 'create' => 'Skapa', + 'update' => 'Uppdatera', + 'edit' => 'Redigera', + 'sort' => 'Sortera', + 'move' => 'Flytta', + 'reply' => 'Svara', + 'delete' => 'Ta bort', + 'search' => 'Sök', + 'search_clear' => 'Rensa sökning', + 'reset' => 'Återställ', + 'remove' => 'Radera', + 'add' => 'Lägg till', + + /** + * Misc + */ + 'deleted_user' => 'Borttagen användare', + 'no_activity' => 'Ingen aktivitet att visa', + 'no_items' => 'Inga tillgängliga föremål', + 'back_to_top' => 'Tillbaka till toppen', + 'toggle_details' => 'Växla detaljer', + 'toggle_thumbnails' => 'Växla miniatyrer', + 'details' => 'Information', + 'grid_view' => 'Rutnätsvy', + 'list_view' => 'Listvy', + + /** + * Header + */ + 'view_profile' => 'Visa profil', + 'edit_profile' => 'Redigera profil', + + /** + * Email Content + */ + 'email_action_help' => 'Om du har problem, klicka på knappen ":actionText", och kopiera och klistra in den här adressen i din webbläsare:', + 'email_rights' => 'Alla rättigheter är reserverade', +]; \ No newline at end of file diff --git a/resources/lang/sv/components.php b/resources/lang/sv/components.php new file mode 100644 index 000000000..7249c5c1f --- /dev/null +++ b/resources/lang/sv/components.php @@ -0,0 +1,32 @@ + 'Val av bild', + 'image_all' => 'Alla', + 'image_all_title' => 'Visa alla bilder', + 'image_book_title' => 'Visa bilder som laddats upp till den aktuella boken', + 'image_page_title' => 'Visa bilder som laddats upp till den aktuella sidan', + 'image_search_hint' => 'Sök efter bildens namn', + 'image_uploaded' => 'Laddades upp :uploadedDate', + 'image_load_more' => 'Ladda fler', + 'image_image_name' => 'Bildnamn', + 'image_delete_confirm' => 'Den här bilden används på nedanstående sidor, klicka på "ta bort" en gång till för att bekräfta att du vill ta bort bilden.', + 'image_select_image' => 'Välj bild', + 'image_dropzone' => 'Släpp bilder här eller klicka för att ladda upp', + 'images_deleted' => 'Bilder borttagna', + 'image_preview' => 'Förhandsgranskning', + 'image_upload_success' => 'Bilden har laddats upp', + 'image_update_success' => 'Bildens uppgifter har ändrats', + 'image_delete_success' => 'Bilden har tagits bort', + + /** + * Code editor + */ + 'code_editor' => 'Redigera kod', + 'code_language' => 'Språk', + 'code_content' => 'Kod', + 'code_save' => 'Spara', +]; \ No newline at end of file diff --git a/resources/lang/sv/entities.php b/resources/lang/sv/entities.php new file mode 100644 index 000000000..d35d3a65a --- /dev/null +++ b/resources/lang/sv/entities.php @@ -0,0 +1,260 @@ + 'Nyligen skapat', + 'recently_created_pages' => 'Sidor som skapats nyligen', + 'recently_updated_pages' => 'Sidor som uppdaterats nyligen', + 'recently_created_chapters' => 'Kapitel som skapats nyligen', + 'recently_created_books' => 'Böcker som skapats nyligen', + 'recently_update' => 'Nyligen uppdaterat', + 'recently_viewed' => 'Nyligen läst', + 'recent_activity' => 'Aktivitet', + 'create_now' => 'Skapa en nu', + 'revisions' => 'Revisioner', + 'meta_revision' => 'Revision #:revisionCount', + 'meta_created' => 'Skapad :timeLength', + 'meta_created_name' => 'Skapad :timeLength av :user', + 'meta_updated' => 'Uppdaterad :timeLength', + 'meta_updated_name' => 'Uppdaterad :timeLength av :user', + 'entity_select' => 'Entity Select', + 'images' => 'Bilder', + 'my_recent_drafts' => 'Mina nyaste utkast', + 'my_recently_viewed' => 'Mina senast visade sidor', + 'no_pages_viewed' => 'Du har inte visat några sidor', + 'no_pages_recently_created' => 'Inga sidor har skapats nyligen', + 'no_pages_recently_updated' => 'Inga sidor har uppdaterats nyligen', + 'export' => 'Exportera', + 'export_html' => 'Webb-fil', + 'export_pdf' => 'PDF-fil', + 'export_text' => 'Textfil', + + /** + * Permissions and restrictions + */ + 'permissions' => 'Rättigheter', + 'permissions_intro' => 'Dessa rättigheter kommer att överskrida eventuella rollbaserade rättigheter.', + 'permissions_enable' => 'Aktivera anpassade rättigheter', + 'permissions_save' => 'Spara rättigheter', + + /** + * Search + */ + 'search_results' => 'Sökresultat', + 'search_total_results_found' => ':count resultat|:count resultat', + 'search_clear' => 'Rensa sökning', + 'search_no_pages' => 'Inga sidor matchade sökningen', + 'search_for_term' => 'Sök efter :term', + 'search_more' => 'Fler resultat', + 'search_filters' => 'Sökfilter', + 'search_content_type' => 'Innehållstyp', + 'search_exact_matches' => 'Exakta matchningar', + 'search_tags' => 'Taggar', + 'search_viewed_by_me' => 'Visade av mig', + 'search_not_viewed_by_me' => 'Ej visade av mig', + 'search_permissions_set' => 'Har anpassade rättigheter', + 'search_created_by_me' => 'Skapade av mig', + 'search_updated_by_me' => 'Uppdaterade av mig', + 'search_updated_before' => 'Uppdaterade före', + 'search_updated_after' => 'Uppdaterade efter', + 'search_created_before' => 'Skapade före', + 'search_created_after' => 'Skapade efter', + 'search_set_date' => 'Ange datum', + 'search_update' => 'Uppdatera sökning', + + /** + * Books + */ + 'book' => 'Bok', + 'books' => 'Böcker', + 'x_books' => ':count bok|:count böcker', + 'books_empty' => 'Inga böcker har skapats', + 'books_popular' => 'Populära böcker', + 'books_recent' => 'Nya böcker', + 'books_new' => 'Nya böcker', + 'books_popular_empty' => 'De mest populära böckerna kommer att visas här.', + 'books_new_empty' => 'De senaste böckerna som skapats kommer att visas här.', + 'books_create' => 'Skapa ny bok', + 'books_delete' => 'Ta bort bok', + 'books_delete_named' => 'Ta bort boken :bookName', + 'books_delete_explain' => 'Du håller på att ta bort boken \':bookName\'. Alla sidor och kapitel kommer också att tas bort.', + 'books_delete_confirmation' => 'Är du säker på att du vill ta bort boken?', + 'books_edit' => 'Redigera bok', + 'books_edit_named' => 'Redigera bok :bookName', + 'books_form_book_name' => 'Bokens namn', + 'books_save' => 'Spara bok', + 'books_permissions' => 'Rättigheter för boken', + 'books_permissions_updated' => 'Bokens rättigheter har uppdaterats', + 'books_empty_contents' => 'Det finns inga sidor eller kapitel i den här boken.', + 'books_empty_create_page' => 'Skapa en ny sida', + 'books_empty_or' => 'eller', + 'books_empty_sort_current_book' => 'Sortera aktuell bok', + 'books_empty_add_chapter' => 'Lägg till kapitel', + 'books_permissions_active' => 'Anpassade rättigheter är i bruk', + 'books_search_this' => 'Sök i boken', + 'books_navigation' => 'Navigering', + 'books_sort' => 'Sortera bokens innehåll', + 'books_sort_named' => 'Sortera boken :bookName', + 'books_sort_show_other' => 'Visa andra böcker', + 'books_sort_save' => 'Spara ordning', + + /** + * Chapters + */ + 'chapter' => 'Kapitel', + 'chapters' => 'Kapitel', + 'x_chapters' => ':count kapitel|:count kapitel', + 'chapters_popular' => 'Populära kapitel', + 'chapters_new' => 'Nytt kapitel', + 'chapters_create' => 'Skapa nytt kapitel', + 'chapters_delete' => 'Radera kapitel', + 'chapters_delete_named' => 'Radera kapitlet :chapterName', + 'chapters_delete_explain' => 'Du håller på att ta bort kapitlet \':chapterName\'. Alla sidor kommer att flyttas direkt in i den aktuella boken istället.', + 'chapters_delete_confirm' => 'Är du säker på att du vill ta bort det här kapitlet?', + 'chapters_edit' => 'Redigera kapitel', + 'chapters_edit_named' => 'Redigera kapitel :chapterName', + 'chapters_save' => 'Spara kapitel', + 'chapters_move' => 'Flytta kapitel', + 'chapters_move_named' => 'Flytta kapitel :chapterName', + 'chapter_move_success' => 'Kapitel flyttat till :bookName', + 'chapters_permissions' => 'Rättigheter för kapitel', + 'chapters_empty' => 'Det finns inga sidor i det här kapitlet.', + 'chapters_permissions_active' => 'Anpassade rättigheter är i bruk', + 'chapters_permissions_success' => 'Rättigheterna för kapitlet har uppdaterats', + 'chapters_search_this' => 'Sök i detta kapitel', + + /** + * Pages + */ + 'page' => 'Sida', + 'pages' => 'Sidor', + 'x_pages' => ':count sida|:count sidor', + 'pages_popular' => 'Populära sidor', + 'pages_new' => 'Ny sida', + 'pages_attachments' => 'Bilagor', + 'pages_navigation' => 'Navigering', + 'pages_delete' => 'Ta bort sida', + 'pages_delete_named' => 'Ta bort sidan :pageName', + 'pages_delete_draft_named' => 'Ta bort utkastet :pageName', + 'pages_delete_draft' => 'Ta bort utkast', + 'pages_delete_success' => 'Sidan har tagits bort', + 'pages_delete_draft_success' => 'Utkastet har tagits bort', + 'pages_delete_confirm' => 'Är du säker på att du vill ta bort den här sidan?', + 'pages_delete_draft_confirm' => 'Är du säker på att du vill ta bort det här utkastet?', + 'pages_editing_named' => 'Redigerar sida :pageName', + 'pages_edit_toggle_header' => 'Växla sidhuvud', + 'pages_edit_save_draft' => 'Spara utkast', + 'pages_edit_draft' => 'Redigera utkast', + 'pages_editing_draft' => 'Redigerar utkast', + 'pages_editing_page' => 'Redigerar sida', + 'pages_edit_draft_save_at' => 'Utkastet sparades ', + 'pages_edit_delete_draft' => 'Ta bort utkast', + 'pages_edit_discard_draft' => 'Ta bort utkastet', + 'pages_edit_set_changelog' => 'Beskriv dina ändringar', + 'pages_edit_enter_changelog_desc' => 'Ange en kort beskrivning av de ändringar du har gjort', + 'pages_edit_enter_changelog' => 'Ändringslogg', + 'pages_save' => 'Spara sida', + 'pages_title' => 'Sidtitel', + 'pages_name' => 'Sidans namn', + 'pages_md_editor' => 'Redigerare', + 'pages_md_preview' => 'Förhandsvisa', + 'pages_md_insert_image' => 'Inoga bild', + 'pages_md_insert_link' => 'Infoga länk', + 'pages_not_in_chapter' => 'Sidan ligger inte i något kapitel', + 'pages_move' => 'Flytta sida', + 'pages_move_success' => 'Sidan har flyttats till ":parentName"', + 'pages_permissions' => 'Rättigheter för sida', + 'pages_permissions_success' => 'Rättigheterna för sidan har uppdaterats', + 'pages_revision' => 'Revision', + 'pages_revisions' => 'Sidrevisioner', + 'pages_revisions_named' => 'Sidrevisioner för :pageName', + 'pages_revision_named' => 'Sidrevision för :pageName', + 'pages_revisions_created_by' => 'Skapad av', + 'pages_revisions_date' => 'Revisionsdatum', + 'pages_revisions_number' => '#', + 'pages_revisions_changelog' => 'Ändringslogg', + 'pages_revisions_changes' => 'Ändringar', + 'pages_revisions_current' => 'Nuvarande version', + 'pages_revisions_preview' => 'Förhandsgranska', + 'pages_revisions_restore' => 'Återställ', + 'pages_revisions_none' => 'Sidan har inga revisioner', + 'pages_copy_link' => 'Kopiera länk', + 'pages_permissions_active' => 'Anpassade rättigheter är i bruk', + 'pages_initial_revision' => 'Första publicering', + 'pages_initial_name' => 'Ny sida', + 'pages_editing_draft_notification' => 'Du redigerar just nu ett utkast som senast sparades :timeDiff.', + 'pages_draft_edited_notification' => 'Denna sida har uppdaterats sen dess. Vi rekommenderar att du förkastar dina ändringar.', + 'pages_draft_edit_active' => [ + 'start_a' => ':count har börjat redigera den här sidan', + 'start_b' => ':userName har börjat redigera den här sidan', + 'time_a' => 'sedan sidan senast uppdaterades', + 'time_b' => 'under de senaste :minCount minuterna', + 'message' => ':start :time. Var försiktiga så att ni inte skriver över varandras ändringar!', + ], + 'pages_draft_discarded' => 'Utkastet har tagits bort. Redigeringsverktyget har uppdaterats med aktuellt innehåll.', + + /** + * Editor sidebar + */ + 'page_tags' => 'Sidtaggar', + 'tag' => 'Tagg', + 'tags' => '', + 'tag_value' => 'Taggvärde (Frivilligt)', + 'tags_explain' => "Lägg till taggar för att kategorisera ditt innehåll bättre. \n Du kan tilldela ett värde till en tagg för ännu bättre organisering.", + 'tags_add' => 'Lägg till ännu en tagg', + 'attachments' => 'Bilagor', + 'attachments_explain' => 'Ladda upp filer eller bifoga länkar till ditt innehåll. Dessa visas i sidokolumnen.', + 'attachments_explain_instant_save' => 'Ändringar här sparas omgående.', + 'attachments_items' => 'Bifogat innehåll', + 'attachments_upload' => 'Ladda upp fil', + 'attachments_link' => 'Bifoga länk', + 'attachments_set_link' => 'Ange länk', + 'attachments_delete_confirm' => 'Klicka på "ta bort" igen för att bekräfta att du vill ta bort bilagan.', + 'attachments_dropzone' => 'Släpp filer här eller klicka för att ladda upp', + 'attachments_no_files' => 'Inga filer har laddats upp', + 'attachments_explain_link' => 'Du kan bifoga en länk om du inte vill ladda upp en fil. Detta kan vara en länk till en annan sida eller till en fil i molnet.', + 'attachments_link_name' => 'Länknamn', + 'attachment_link' => 'Länk till bilaga', + 'attachments_link_url' => 'Länk till fil', + 'attachments_link_url_hint' => 'URL till sida eller fil', + 'attach' => 'Bifoga', + 'attachments_edit_file' => 'Redigera fil', + 'attachments_edit_file_name' => 'Filnamn', + 'attachments_edit_drop_upload' => 'Släpp filer här eller klicka för att ladda upp och skriva över', + 'attachments_order_updated' => 'Ordningen på bilagorna har uppdaterats', + 'attachments_updated_success' => 'Bilagan har uppdaterats', + 'attachments_deleted' => 'Bilagan har tagits bort', + 'attachments_file_uploaded' => 'Filen har laddats upp', + 'attachments_file_updated' => 'Filen har uppdaterats', + 'attachments_link_attached' => 'Länken har bifogats till sidan', + + /** + * Profile View + */ + 'profile_user_for_x' => 'Användare i :time', + 'profile_created_content' => 'Skapat innehåll', + 'profile_not_created_pages' => ':userName har inte skapat några sidor', + 'profile_not_created_chapters' => ':userName har inte skapat några kapitel', + 'profile_not_created_books' => ':userName har inte skapat några böcker', + + /** + * Comments + */ + 'comment' => 'Kommentar', + 'comments' => 'Kommentarer', + 'comment_placeholder' => 'Lämna en kommentar här', + 'comment_count' => '{0} Inga kommentarer|{1} 1 kommentar|[2,*] :count kommentarer', + 'comment_save' => 'Spara kommentar', + 'comment_saving' => 'Sparar kommentar...', + 'comment_deleting' => 'Tar bort kommentar...', + 'comment_new' => 'Ny kommentar', + 'comment_created' => 'kommenterade :createDiff', + 'comment_updated' => 'Uppdaterade :updateDiff av :username', + 'comment_deleted_success' => 'Kommentar borttagen', + 'comment_created_success' => 'Kommentaren har sparats', + 'comment_updated_success' => 'Kommentaren har uppdaterats', + 'comment_delete_confirm' => 'Är du säker på att du vill ta bort den här kommentaren?', + 'comment_in_reply_to' => 'Som svar på :commentId', +]; \ No newline at end of file diff --git a/resources/lang/sv/errors.php b/resources/lang/sv/errors.php new file mode 100644 index 000000000..4dfc149f0 --- /dev/null +++ b/resources/lang/sv/errors.php @@ -0,0 +1,79 @@ + 'Du har inte tillgång till den här sidan.', + 'permissionJson' => 'Du har inte rätt att utföra den här åtgärden.', + + // Auth + 'error_user_exists_different_creds' => 'En användare med adressen :email finns redan.', + 'email_already_confirmed' => 'E-posten har redan bekräftats, prova att logga in.', + 'email_confirmation_invalid' => 'Denna bekräftelsekod är inte giltig eller har redan använts. Vänligen prova att registera dig på nytt', + 'email_confirmation_expired' => 'Denna bekräftelsekod har gått ut. Vi har skickat dig en ny.', + 'ldap_fail_anonymous' => 'LDAP-inloggning misslyckades med anonym bindning', + 'ldap_fail_authed' => 'LDAP-inloggning misslyckades med angivna dn- och lösenordsuppgifter', + 'ldap_extension_not_installed' => 'LDAP PHP-tillägg inte installerat', + 'ldap_cannot_connect' => 'Kan inte ansluta till ldap-servern. Anslutningen misslyckades', + 'social_no_action_defined' => 'Ingen åtgärd definierad', + 'social_login_bad_response' => "Ett fel inträffade vid inloggning genom :socialAccount: \n:error", + 'social_account_in_use' => 'Detta konto från :socialAccount används redan. Testa att logga in med :socialAccount istället.', + 'social_account_email_in_use' => 'E-posten :email används redan. Om du redan har ett konto kan du ansluta ditt konto från :socialAccount via dina profilinställningar.', + 'social_account_existing' => 'Detta konto från :socialAccount är redan länkat till din profil.', + 'social_account_already_used_existing' => 'Detta konto från :socialAccount används redan av en annan användare.', + 'social_account_not_used' => 'Detta konto från :socialAccount är inte länkat till någon användare. Vänligen anslut via dina profilinställningar. ', + 'social_account_register_instructions' => 'Om du inte har något konto ännu kan du registerar dig genom att välja :socialAccount.', + 'social_driver_not_found' => 'Drivrutinen för den här tjänsten hittades inte', + 'social_driver_not_configured' => 'Dina inställningar för :socialAccount är inte korrekta.', + + // System + 'path_not_writable' => 'Kunde inte ladda upp till sökvägen :filePath. Kontrollera att webbservern har skrivåtkomst.', + 'cannot_get_image_from_url' => 'Kan inte hämta bild från :url', + 'cannot_create_thumbs' => 'Servern kan inte skapa miniatyrer. Kontrollera att du har PHPs GD-tillägg aktiverat.', + 'server_upload_limit' => 'Servern tillåter inte så här stora filer. Prova en mindre fil.', + 'image_upload_error' => 'Ett fel inträffade vid uppladdningen', + + // Attachments + 'attachment_page_mismatch' => 'Fel i sidmatchning vid uppdatering av bilaga', + + // Pages + 'page_draft_autosave_fail' => 'Kunde inte spara utkastet. Kontrollera att du är ansluten till internet.', + 'page_custom_home_deletion' => 'Det går inte att ta bort sidan medan den används som startsida', + + // Entities + 'entity_not_found' => 'Innehållet hittades inte', + 'book_not_found' => 'Boken hittades inte', + 'page_not_found' => 'Sidan hittades inte', + 'chapter_not_found' => 'Kapitlet hittades inte', + 'selected_book_not_found' => 'Den valda boken hittades inte', + 'selected_book_chapter_not_found' => 'Den valda boken eller kapitlet hittades inte', + 'guests_cannot_save_drafts' => 'Gäster kan inte spara utkast', + + // Users + 'users_cannot_delete_only_admin' => 'Du kan inte ta bort den enda admin-användaren', + 'users_cannot_delete_guest' => 'Du kan inte ta bort gästanvändaren', + + // Roles + 'role_cannot_be_edited' => 'Den här rollen kan inte redigeras', + 'role_system_cannot_be_deleted' => 'Det här är en systemroll och kan därför inte tas bort', + 'role_registration_default_cannot_delete' => 'Det går inte att ta bort rollen medan den används som standardroll.', + + // Comments + 'comment_list' => 'Ett fel inträffade då kommentarer skulle hämtas.', + 'cannot_add_comment_to_draft' => 'Du kan inte kommentera ett utkast.', + 'comment_add' => 'Ett fel inträffade då kommentaren skulle sparas.', + 'comment_delete' => 'Ett fel inträffade då kommentaren skulle tas bort.', + 'empty_comment' => 'Kan inte lägga till en tom kommentar.', + + // Error pages + '404_page_not_found' => 'Sidan hittades inte', + 'sorry_page_not_found' => 'Tyvärr gick det inte att hitta sidan du söker.', + 'return_home' => 'Återvänd till startsidan', + 'error_occurred' => 'Ett fel inträffade', + 'app_down' => ':appName är nere just nu', + 'back_soon' => 'Vi är snart tillbaka.', +]; \ No newline at end of file diff --git a/resources/lang/sv/pagination.php b/resources/lang/sv/pagination.php new file mode 100644 index 000000000..aa12db17c --- /dev/null +++ b/resources/lang/sv/pagination.php @@ -0,0 +1,19 @@ + '« Föregående', + 'next' => 'Nästa »', + +]; diff --git a/resources/lang/sv/passwords.php b/resources/lang/sv/passwords.php new file mode 100644 index 000000000..1f33f550b --- /dev/null +++ b/resources/lang/sv/passwords.php @@ -0,0 +1,22 @@ + 'Lösenord måste vara minst sex tecken långa och anges likadant två gånger.', + 'user' => "Det finns ingen användare med den e-postadressen.", + 'token' => 'Återställningskoden är ogiltig.', + 'sent' => 'Vi har mailat dig en länk för att återställa ditt lösenord!', + 'reset' => 'Ditt lösenord har blivit återställt!', + +]; diff --git a/resources/lang/sv/settings.php b/resources/lang/sv/settings.php new file mode 100644 index 000000000..b0496c924 --- /dev/null +++ b/resources/lang/sv/settings.php @@ -0,0 +1,138 @@ + 'Inställningar', + 'settings_save' => 'Spara inställningar', + 'settings_save_success' => 'Inställningarna har sparats', + + /** + * App settings + */ + + 'app_settings' => 'Appinställningar', + 'app_name' => 'Applikationsnamn', + 'app_name_desc' => 'Namnet visas i sidhuvdet och i eventuella mail.', + 'app_name_header' => 'Visa applikationsnamn i sidhuvudet?', + 'app_public_viewing' => 'Tillåt publikt innehåll?', + 'app_secure_images' => 'Aktivera högre säkerhet för bilduppladdningar?', + 'app_secure_images_desc' => 'Av prestandaskäl är alla bilder publika. Det här alternativet lägger till en slumpmässig, svårgissad sträng framför alla bild-URL:er. Se till att kataloglistning inte är aktivt för att förhindra åtkomst.', + 'app_editor' => 'Redigeringsverktyg', + 'app_editor_desc' => 'Välj vilket redigeringsverktyg som ska användas av alla användare för att redigera sidor.', + 'app_custom_html' => 'Egen HTML i ', + 'app_custom_html_desc' => 'Eventuellt innehåll i det här fältet placeras längst ner i -sektionen på varje sida. Detta är användbart för att skriva över stilmaller eller lägga in spårningskoder.', + 'app_logo' => 'Applikationslogotyp', + 'app_logo_desc' => 'Bilden bör vara minst 43px hög.
Större bilder skalas ner.', + 'app_primary_color' => 'Primärfärg', + 'app_primary_color_desc' => 'Detta ska vara en hexadimal färgkod.
Lämna tomt för att återställa standardfärgen.', + 'app_homepage' => 'Startsida', + 'app_homepage_desc' => 'Välj en sida att använda som startsida istället för standardvyn. Den valda sidans rättigheter kommer att ignoreras.', + 'app_homepage_default' => 'Vald vy för startsida', + 'app_disable_comments' => 'Inaktivera kommentarer', + 'app_disable_comments_desc' => 'Inaktivera kommentarer på alla sidor i applikationen. Befintliga kommentarer visas inte.', + + /** + * Registration settings + */ + + 'reg_settings' => 'Registreringsinställningar', + 'reg_allow' => 'Tillåt registrering?', + 'reg_default_role' => 'Standardroll efter registrering', + 'reg_confirm_email' => 'Kräv e-postbekräftelse?', + 'reg_confirm_email_desc' => 'Om registrering begränas till vissa domäner kommer e-postbekräftelse alltid att krävas och den här inställningen kommer att ignoreras.', + 'reg_confirm_restrict_domain' => 'Begränsa registrering till viss domän', + 'reg_confirm_restrict_domain_desc' => 'Ange en kommaseparerad lista över e-postdomäner till vilka du vill begränsa registrering. Användare kommer att skickas ett mail för att bekräfta deras e-post innan de får logga in.
Notera att användare kommer att kunna ändra sin e-postadress efter lyckad registrering.', + 'reg_confirm_restrict_domain_placeholder' => 'Ingen begränsning satt', + + /** + * Role settings + */ + + 'roles' => 'Roller', + 'role_user_roles' => 'Användarroller', + 'role_create' => 'Skapa ny roll', + 'role_create_success' => 'Rollen har skapats', + 'role_delete' => 'Ta bort roll', + 'role_delete_confirm' => 'Rollen med namn \':roleName\' kommer att tas bort.', + 'role_delete_users_assigned' => 'Det finns :userCount användare som tillhör den här rollen. Om du vill migrera användarna från den här rollen, välj en ny roll nedan.', + 'role_delete_no_migration' => 'Migrera inte användare', + 'role_delete_sure' => 'Är du säker på att du vill ta bort den här rollen?', + 'role_delete_success' => 'Rollen har tagits bort', + 'role_edit' => 'Redigera roll', + 'role_details' => 'Om rollen', + 'role_name' => 'Rollens namn', + 'role_desc' => 'Kort beskrivning av rollen', + 'role_system' => 'Systemrättigheter', + 'role_manage_users' => 'Hanter användare', + 'role_manage_roles' => 'Hantera roller & rättigheter', + 'role_manage_entity_permissions' => 'Hantera rättigheter för alla böcker, kapitel och sidor', + 'role_manage_own_entity_permissions' => 'Hantera rättigheter för egna böcker, kapitel och sidor', + 'role_manage_settings' => 'Hantera appinställningar', + 'role_asset' => 'Tillgång till innehåll', + 'role_asset_desc' => 'Det här är standardinställningarna för allt innehåll i systemet. Eventuella anpassade rättigheter på böcker, kapitel och sidor skriver över dessa inställningar.', + 'role_all' => 'Alla', + 'role_own' => 'Egna', + 'role_controlled_by_asset' => 'Kontrolleras av den sida de laddas upp till', + 'role_save' => 'Spara roll', + 'role_update_success' => 'Rollen har uppdaterats', + 'role_users' => 'Användare med denna roll', + 'role_users_none' => 'Inga användare tillhör den här rollen', + + /** + * Users + */ + + 'users' => 'Användare', + 'user_profile' => 'Användarprofil', + 'users_add_new' => 'Lägg till användare', + 'users_search' => 'Sök användare', + 'users_role' => 'Användarroller', + 'users_external_auth_id' => 'Externt ID för autentisering', + 'users_password_warning' => 'Fyll i nedanstående fält endast om du vill byta lösenord:', + 'users_system_public' => 'Den här användaren representerar eventuella gäster som använder systemet. Den kan inte användas för att logga in utan tilldeles automatiskt.', + 'users_books_view_type' => 'Layout för visning av böcker', + 'users_delete' => 'Ta bort användare', + 'users_delete_named' => 'Ta bort användaren :userName', + 'users_delete_warning' => 'Detta kommer att ta bort användaren \':userName\' från systemet helt och hållet.', + 'users_delete_confirm' => 'Är du säker på att du vill ta bort användaren?', + 'users_delete_success' => 'Användaren har tagits bort', + 'users_edit' => 'Redigera användare', + 'users_edit_profile' => 'Redigera profil', + 'users_edit_success' => 'Användaren har uppdaterats', + 'users_avatar' => 'Avatar', + 'users_avatar_desc' => 'Bilden bör vara kvadratisk och ca 256px stor.', + 'users_preferred_language' => 'Språk', + 'users_social_accounts' => 'Anslutna konton', + 'users_social_accounts_info' => 'Här kan du ansluta dina andra konton för snabbare och smidigare inloggning. Om du kopplar från en tjänst här kommer de behörigheter som tidigare givits inte att tas bort - ta bort behörigheter genom att logga in på ditt konto på tjänsten i fråga.', + 'users_social_connect' => 'Anslut konto', + 'users_social_disconnect' => 'Koppla från konto', + 'users_social_connected' => ':socialAccount har kopplats till ditt konto.', + 'users_social_disconnected' => ':socialAccount har kopplats bort från ditt konto.', + + // Since these labels are already localized this array does not need to be + // translated in the language-specific files. + // DELETE BELOW IF COPIED FROM EN + /////////////////////////////////// + 'language_select' => [ + 'en' => 'English', + 'de' => 'Deutsch', + 'es' => 'Español', + 'es_AR' => 'Español Argentina', + 'fr' => 'Français', + 'nl' => 'Nederlands', + 'pt_BR' => 'Português do Brasil', + 'sk' => 'Slovensky', + 'sv' => 'Svenska', + 'ja' => '日本語', + 'pl' => 'Polski', + 'it' => 'Italian', + 'ru' => 'Русский' + ] + /////////////////////////////////// +]; diff --git a/resources/lang/sv/validation.php b/resources/lang/sv/validation.php new file mode 100644 index 000000000..649d843bb --- /dev/null +++ b/resources/lang/sv/validation.php @@ -0,0 +1,108 @@ + ':attribute måste godkännas.', + 'active_url' => ':attribute är inte en giltig URL.', + 'after' => ':attribute måste vara efter :date.', + 'alpha' => ':attribute får bara innehålla bokstäver.', + 'alpha_dash' => ':attribute får bara innehålla bokstäver, siffror och bindestreck.', + 'alpha_num' => ':attribute får bara innehålla bokstäver och siffror.', + 'array' => ':attribute måste vara en array.', + 'before' => ':attribute måste vara före :date.', + 'between' => [ + 'numeric' => ':attribute måste vara mellan :min och :max.', + 'file' => ':attribute måste vara mellan :min och :max kilobyte stor.', + 'string' => ':attribute måste vara mellan :min och :max tecken.', + 'array' => ':attribute måste innehålla mellan :min och :max poster.', + ], + 'boolean' => ':attribute måste vara sant eller falskt.', + 'confirmed' => 'Bekräftelsen av :attribute stämmer inte.', + 'date' => ':attribute är inte ett giltigt datum.', + 'date_format' => ':attribute matchar inte formatet :format.', + 'different' => ':attribute och :other måste vara olika.', + 'digits' => ':attribute måste vara :digits siffror.', + 'digits_between' => ':attribute måste vara mellan :min och :max siffror.', + 'email' => ':attribute måste vara en giltig e-postadress.', + 'filled' => ':attribute är obligatoriskt.', + 'exists' => 'Valt värde för :attribute är ogiltigt.', + 'image' => ':attribute måste vara en bild.', + 'in' => 'Vald :attribute är ogiltigt.', + 'integer' => ':attribute måste vara en integer.', + 'ip' => ':attribute måste vara en giltig IP-adress.', + 'max' => [ + 'numeric' => ':attribute får inte vara större än :max.', + 'file' => ':attribute får inte vara större än :max kilobyte.', + 'string' => ':attribute får inte vara längre än :max tecken.', + 'array' => ':attribute får inte ha fler än :max poster.', + ], + 'mimes' => ':attribute måste vara en fil av typen: :values.', + 'min' => [ + 'numeric' => ':attribute måste vara minst :min.', + 'file' => ':attribute måste vara minst :min kilobyte stor.', + 'string' => ':attribute måste vara minst :min tecken.', + 'array' => ':attribute måste ha minst :min poster.', + ], + 'not_in' => 'Vald :attribute är inte giltig', + 'numeric' => ':attribute måste vara ett nummer.', + 'regex' => ':attribute har ett ogiltigt format.', + 'required' => ':attribute är obligatoriskt.', + 'required_if' => ':attribute är obligatoriskt när :other är :value.', + 'required_with' => ':attribute är obligatoriskt när :values finns.', + 'required_with_all' => ':attribute är obligatoriskt när :values finns.', + 'required_without' => ':attribute är obligatoriskt när :values inte finns.', + 'required_without_all' => ':attribute är obligatirskt när ingen av :values finns.', + 'same' => ':attribute och :other måste stämma överens.', + 'size' => [ + 'numeric' => ':attribute måste vara :size.', + 'file' => ':attribute måste vara :size kilobyte.', + 'string' => ':attribute måste vara :size tecken.', + 'array' => ':attribute måste innehålla :size poster.', + ], + 'string' => ':attribute måste vara en sträng.', + 'timezone' => ':attribute måste vara en giltig tidszon.', + 'unique' => ':attribute är upptaget', + 'url' => 'Formatet på :attribute är ogiltigt.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'password-confirm' => [ + 'required_with' => 'Lösenordet måste bekräftas', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/lang/zh_CN/activities.php b/resources/lang/zh_CN/activities.php new file mode 100644 index 000000000..6ef10b67f --- /dev/null +++ b/resources/lang/zh_CN/activities.php @@ -0,0 +1,42 @@ + '创建了页面', + 'page_create_notification' => '页面已创建成功', + 'page_update' => '更新了页面', + 'page_update_notification' => '页面已更新成功', + 'page_delete' => '删除了页面', + 'page_delete_notification' => '页面已删除成功', + 'page_restore' => '恢复了页面', + 'page_restore_notification' => '页面已恢复成功', + 'page_move' => '移动了页面', + + // Chapters + 'chapter_create' => '创建了章节', + 'chapter_create_notification' => '章节已创建成功', + 'chapter_update' => '更新了章节', + 'chapter_update_notification' => '章节已创建成功', + 'chapter_delete' => '删除了章节', + 'chapter_delete_notification' => '章节已删除成功', + 'chapter_move' => '移动了章节', + + // Books + 'book_create' => '创建了图书', + 'book_create_notification' => '图书已创建成功', + 'book_update' => '更新了图书', + 'book_update_notification' => '图书已更新成功', + 'book_delete' => '删除了图书', + 'book_delete_notification' => '图书已删除成功', + 'book_sort' => '排序了图书', + 'book_sort_notification' => '图书已重新排序成功', + + // Other + 'commented_on' => '评论', +]; diff --git a/resources/lang/zh_CN/auth.php b/resources/lang/zh_CN/auth.php new file mode 100644 index 000000000..046f2360b --- /dev/null +++ b/resources/lang/zh_CN/auth.php @@ -0,0 +1,76 @@ + '用户名或密码错误。', + 'throttle' => '您的登录次数过多,请在:seconds秒后重试。', + + /** + * Login & Register + */ + 'sign_up' => '注册', + 'log_in' => '登录', + 'log_in_with' => '以:socialDriver登录', + 'sign_up_with' => '注册:socialDriver', + 'logout' => '注销', + + 'name' => '姓名', + 'username' => '用户名', + 'email' => 'Email地址', + 'password' => '密码', + 'password_confirm' => '确认密码', + 'password_hint' => '必须超过5个字符', + 'forgot_password' => '忘记密码?', + 'remember_me' => '记住我', + 'ldap_email_hint' => '请输入用于此帐户的电子邮件。', + 'create_account' => '创建账户', + 'social_login' => 'SNS登录', + 'social_registration' => 'SNS注册', + 'social_registration_text' => '其他服务注册/登录.', + + 'register_thanks' => '注册完成!', + 'register_confirm' => '请点击查收您的Email,并点击确认。', + 'registrations_disabled' => '注册目前被禁用', + 'registration_email_domain_invalid' => '该Email域名无权访问此应用程序', + 'register_success' => '感谢您注册:appName,您现在已经登录。', + + + /** + * Password Reset + */ + 'reset_password' => '重置密码', + 'reset_password_send_instructions' => '在下面输入您的Email地址,您将收到一封带有密码重置链接的邮件。', + 'reset_password_send_button' => '发送重置链接', + 'reset_password_sent_success' => '密码重置链接已发送到:email。', + 'reset_password_success' => '您的密码已成功重置。', + + 'email_reset_subject' => '重置您的:appName密码', + 'email_reset_text' => '您收到此电子邮件是因为我们收到了您的帐户的密码重置请求。', + 'email_reset_not_requested' => '如果您没有要求重置密码,则不需要采取进一步的操作。', + + + /** + * Email Confirmation + */ + 'email_confirm_subject' => '确认您在:appName的Email地址', + 'email_confirm_greeting' => '感谢您加入:appName!', + 'email_confirm_text' => '请点击下面的按钮确认您的Email地址:', + 'email_confirm_action' => '确认Email', + 'email_confirm_send_error' => '需要Email验证,但系统无法发送电子邮件,请联系网站管理员。', + 'email_confirm_success' => '您的Email地址已成功验证!', + 'email_confirm_resent' => '验证邮件已重新发送,请检查收件箱。', + + 'email_not_confirmed' => 'Email地址未验证', + 'email_not_confirmed_text' => '您的电子邮件地址尚未确认。', + 'email_not_confirmed_click_link' => '请检查注册时收到的电子邮件,然后点击确认链接。', + 'email_not_confirmed_resend' => '如果找不到电子邮件,请通过下面的表单重新发送确认Email。', + 'email_not_confirmed_resend_button' => '重新发送确认Email', +]; \ No newline at end of file diff --git a/resources/lang/zh_CN/common.php b/resources/lang/zh_CN/common.php new file mode 100644 index 000000000..a6efde672 --- /dev/null +++ b/resources/lang/zh_CN/common.php @@ -0,0 +1,64 @@ + '取消', + 'confirm' => '确认', + 'back' => '返回', + 'save' => '保存', + 'continue' => '继续', + 'select' => '选择', + 'more' => '更多', + + /** + * Form Labels + */ + 'name' => '姓名', + 'description' => '概要', + 'role' => '角色', + 'cover_image' => '封面图片', + 'cover_image_description' => '该图像大小需要为440x250px。', + + /** + * Actions + */ + 'actions' => '操作', + 'view' => '视图', + 'create' => '创建', + 'update' => '更新', + 'edit' => '编辑', + 'sort' => '排序', + 'move' => '移动', + 'reply' => '回复', + 'delete' => '删除', + 'search' => '搜索', + 'search_clear' => '清除搜索', + 'reset' => '重置', + 'remove' => '删除', + 'add' => '添加', + + /** + * Misc + */ + 'deleted_user' => '删除用户', + 'no_activity' => '没有活动要显示', + 'no_items' => '没有可用的项目', + 'back_to_top' => '回到顶部', + 'toggle_details' => '显示/隐藏详细信息', + 'toggle_thumbnails' => '显示/隐藏缩略图', + 'details' => '详细信息', + + /** + * Header + */ + 'view_profile' => '查看资料', + 'edit_profile' => '编辑资料', + + /** + * Email Content + */ + 'email_action_help' => '如果您无法点击“:actionText”按钮,请将下面的网址复制到您的浏览器中打开:', + 'email_rights' => 'All rights reserved', +]; diff --git a/resources/lang/zh_CN/components.php b/resources/lang/zh_CN/components.php new file mode 100644 index 000000000..9e6f28649 --- /dev/null +++ b/resources/lang/zh_CN/components.php @@ -0,0 +1,32 @@ + '选择图片', + 'image_all' => '全部', + 'image_all_title' => '查看所有图片', + 'image_book_title' => '查看上传到本书的图片', + 'image_page_title' => '查看上传到本页面的图片', + 'image_search_hint' => '按图片名称搜索', + 'image_uploaded' => '上传于 :uploadedDate', + 'image_load_more' => '显示更多', + 'image_image_name' => '图片名称', + 'image_delete_confirm' => '该图像用于以下页面,如果你想删除它,请再次按下按钮。', + 'image_select_image' => '选择图片', + 'image_dropzone' => '拖放图片或点击此处上传', + 'images_deleted' => '图片已删除', + 'image_preview' => '图片预览', + 'image_upload_success' => '图片上传成功', + 'image_update_success' => '图片详细信息更新成功', + 'image_delete_success' => '图片删除成功', + + /** + * Code editor + */ + 'code_editor' => '编辑代码', + 'code_language' => '编程语言', + 'code_content' => '代码内容', + 'code_save' => '保存代码', +]; \ No newline at end of file diff --git a/resources/lang/zh_CN/entities.php b/resources/lang/zh_CN/entities.php new file mode 100644 index 000000000..3c04c442d --- /dev/null +++ b/resources/lang/zh_CN/entities.php @@ -0,0 +1,260 @@ + '最近创建', + 'recently_created_pages' => '最近创建的页面', + 'recently_updated_pages' => '最新页面', + 'recently_created_chapters' => '最近创建的章节', + 'recently_created_books' => '最近创建的图书', + 'recently_update' => '最近更新', + 'recently_viewed' => '最近查看', + 'recent_activity' => '近期活动', + 'create_now' => '立刻创建', + 'revisions' => '修订历史', + 'meta_revision' => '版本号 #:revisionCount', + 'meta_created' => '创建于 :timeLength', + 'meta_created_name' => '由 :user 创建于 :timeLength', + 'meta_updated' => '更新于 :timeLength', + 'meta_updated_name' => '由 :user 更新于 :timeLength', + 'entity_select' => '实体选择', + 'images' => '图片', + 'my_recent_drafts' => '我最近的草稿', + 'my_recently_viewed' => '我最近看过', + 'no_pages_viewed' => '您尚未查看任何页面', + 'no_pages_recently_created' => '最近没有页面被创建', + 'no_pages_recently_updated' => '最近没有页面被更新', + 'export' => '导出', + 'export_html' => '网页文件', + 'export_pdf' => 'PDF文件', + 'export_text' => '纯文本文件', + + /** + * Permissions and restrictions + */ + 'permissions' => '权限', + 'permissions_intro' => '本设置优先于每个用户角色本身所具有的权限。', + 'permissions_enable' => '启用自定义权限', + 'permissions_save' => '保存权限', + + /** + * Search + */ + 'search_results' => '搜索结果', + 'search_total_results_found' => '共找到了:count个结果', + 'search_clear' => '清除搜索', + 'search_no_pages' => '没有找到相匹配的页面', + 'search_for_term' => '“:term”的搜索结果', + 'search_more' => '更多结果', + 'search_filters' => '过滤搜索结果', + 'search_content_type' => '种类', + 'search_exact_matches' => '精确匹配', + 'search_tags' => '标签搜索', + 'search_viewed_by_me' => '我看过的', + 'search_not_viewed_by_me' => '我没看过的', + 'search_permissions_set' => '权限设置', + 'search_created_by_me' => '我创建的', + 'search_updated_by_me' => '我更新的', + 'search_updated_before' => '在此之前更新', + 'search_updated_after' => '在此之后更新', + 'search_created_before' => '在此之前创建', + 'search_created_after' => '在此之后创建', + 'search_set_date' => '设置日期', + 'search_update' => '只显示更新操作', + + /** + * Books + */ + 'book' => '图书', + 'books' => '图书', + 'x_books' => ':count本书', + 'books_empty' => '不存在已创建的书', + 'books_popular' => '热门图书', + 'books_recent' => '最近的书', + 'books_new' => '新书', + 'books_popular_empty' => '最受欢迎的图书将出现在这里。', + 'books_new_empty' => '最近创建的图书将出现在这里。', + 'books_create' => '创建图书', + 'books_delete' => '删除图书', + 'books_delete_named' => '删除图书「:bookName」', + 'books_delete_explain' => '这将删除图书「:bookName」。所有的章节和页面都会被删除。', + 'books_delete_confirmation' => '您确定要删除此图书吗?', + 'books_edit' => '编辑图书', + 'books_edit_named' => '编辑图书「:bookName」', + 'books_form_book_name' => '书名', + 'books_save' => '保存图书', + 'books_permissions' => '图书权限', + 'books_permissions_updated' => '图书权限已更新', + 'books_empty_contents' => '本书目前没有页面或章节。', + 'books_empty_create_page' => '创建页面', + 'books_empty_or' => '或', + 'books_empty_sort_current_book' => '排序当前图书', + 'books_empty_add_chapter' => '添加章节', + 'books_permissions_active' => '有效的图书权限', + 'books_search_this' => '搜索这本书', + 'books_navigation' => '图书导航', + 'books_sort' => '排序图书内容', + 'books_sort_named' => '排序图书「:bookName」', + 'books_sort_show_other' => '显示其他图书', + 'books_sort_save' => '保存新顺序', + + /** + * Chapters + */ + 'chapter' => '章节', + 'chapters' => '章节', + 'x_chapters' => ':count个章节', + 'chapters_popular' => '热门章节', + 'chapters_new' => '新章节', + 'chapters_create' => '创建章节', + 'chapters_delete' => '删除章节', + 'chapters_delete_named' => '删除章节「:chapterName」', + 'chapters_delete_explain' => '这将删除章节「:chapterName」。所有的页面将被删除并添加到其所在的书籍。', + 'chapters_delete_confirm' => '您确定要删除此章节吗?', + 'chapters_edit' => '编辑章节', + 'chapters_edit_named' => '编辑章节「:chapterName」', + 'chapters_save' => '保存章节', + 'chapters_move' => '移动章节', + 'chapters_move_named' => '移动章节「:chapterName」', + 'chapter_move_success' => '章节移动到「:bookName」', + 'chapters_permissions' => '章节权限', + 'chapters_empty' => '本章目前没有页面。', + 'chapters_permissions_active' => '有效的章节权限', + 'chapters_permissions_success' => '章节权限已更新', + 'chapters_search_this' => '从本章节搜索', + + /** + * Pages + */ + 'page' => '页面', + 'pages' => '页面', + 'x_pages' => ':count个页面', + 'pages_popular' => '热门页面', + 'pages_new' => '新页面', + 'pages_attachments' => '附件', + 'pages_navigation' => '页面导航', + 'pages_delete' => '删除页面', + 'pages_delete_named' => '删除页面“:pageName”', + 'pages_delete_draft_named' => '删除草稿页面“:pageName”', + 'pages_delete_draft' => '删除草稿页面', + 'pages_delete_success' => '页面已删除', + 'pages_delete_draft_success' => '草稿页面已删除', + 'pages_delete_confirm' => '您确定要删除此页面吗?', + 'pages_delete_draft_confirm' => '您确定要删除此草稿页面吗?', + 'pages_editing_named' => '正在编辑页面“:pageName”', + 'pages_edit_toggle_header' => '显示/隐藏导航栏', + 'pages_edit_save_draft' => '保存草稿', + 'pages_edit_draft' => '编辑页面草稿', + 'pages_editing_draft' => '正在编辑草稿', + 'pages_editing_page' => '正在编辑页面', + 'pages_edit_draft_save_at' => '草稿保存于 ', + 'pages_edit_delete_draft' => '删除草稿', + 'pages_edit_discard_draft' => '放弃草稿', + 'pages_edit_set_changelog' => '更新说明', + 'pages_edit_enter_changelog_desc' => '输入对您所做更改的简要说明', + 'pages_edit_enter_changelog' => '输入更新说明', + 'pages_save' => '保存页面', + 'pages_title' => '页面标题', + 'pages_name' => '页面名', + 'pages_md_editor' => '编者', + 'pages_md_preview' => '预览', + 'pages_md_insert_image' => '插入图片', + 'pages_md_insert_link' => '插入实体链接', + 'pages_not_in_chapter' => '本页面不在某章节中', + 'pages_move' => '移动页面', + 'pages_move_success' => '页面已移动到「:parentName」', + 'pages_permissions' => '页面权限', + 'pages_permissions_success' => '页面权限已更新', + 'pages_revision' => '修订', + 'pages_revisions' => '页面修订', + 'pages_revisions_named' => '“:pageName”页面修订', + 'pages_revision_named' => '“:pageName”页面修订', + 'pages_revisions_created_by' => '创建者', + 'pages_revisions_date' => '修订日期', + 'pages_revisions_number' => '#', + 'pages_revisions_changelog' => '更新说明', + 'pages_revisions_changes' => '说明', + 'pages_revisions_current' => '当前版本', + 'pages_revisions_preview' => '预览', + 'pages_revisions_restore' => '恢复', + 'pages_revisions_none' => '此页面没有修订', + 'pages_copy_link' => '复制链接', + 'pages_permissions_active' => '有效的页面权限', + 'pages_initial_revision' => '初始发布', + 'pages_initial_name' => '新页面', + 'pages_editing_draft_notification' => '您正在编辑在 :timeDiff 内保存的草稿.', + 'pages_draft_edited_notification' => '此后,此页面已经被更新,建议您放弃此草稿。', + 'pages_draft_edit_active' => [ + 'start_a' => ':count位用户正在编辑此页面', + 'start_b' => '用户“:userName”已经开始编辑此页面', + 'time_a' => '自页面上次更新以来', + 'time_b' => '在最近:minCount分钟', + 'message' => ':time,:start。注意不要覆盖对方的更新!', + ], + 'pages_draft_discarded' => '草稿已丢弃,编辑器已更新到当前页面内容。', + + /** + * Editor sidebar + */ + 'page_tags' => '页面标签', + 'tag' => '标签', + 'tags' => '', + 'tag_value' => '标签值 (Optional)', + 'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更深入的组织。", + 'tags_add' => '添加另一个标签', + 'attachments' => '附件', + 'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。', + 'attachments_explain_instant_save' => '这里的更改将立即保存。Changes here are saved instantly.', + 'attachments_items' => '附加项目', + 'attachments_upload' => '上传文件', + 'attachments_link' => '附加链接', + 'attachments_set_link' => '设置链接', + 'attachments_delete_confirm' => '确认您想要删除此附件后,请点击删除。', + 'attachments_dropzone' => '删除文件或点击此处添加文件', + 'attachments_no_files' => '尚未上传文件', // No files have been uploaded + 'attachments_explain_link' => '如果您不想上传文件,则可以附加链接,这可以是指向其他页面的链接,也可以是指向云端文件的链接。', + 'attachments_link_name' => '链接名', + 'attachment_link' => '附件链接', + 'attachments_link_url' => '链接到文件', + 'attachments_link_url_hint' => '网站或文件的网址', + 'attach' => '附加', + 'attachments_edit_file' => '编辑文件', + 'attachments_edit_file_name' => '文件名', + 'attachments_edit_drop_upload' => '删除文件或点击这里上传并覆盖', + 'attachments_order_updated' => '附件顺序已更新', + 'attachments_updated_success' => '附件信息已更新', + 'attachments_deleted' => '附件已删除', + 'attachments_file_uploaded' => '附件上传成功', + 'attachments_file_updated' => '附件更新成功', + 'attachments_link_attached' => '链接成功附加到页面', + + /** + * Profile View + */ + 'profile_user_for_x' => '来这里:time了', + 'profile_created_content' => '已创建内容', + 'profile_not_created_pages' => ':userName尚未创建任何页面', + 'profile_not_created_chapters' => ':userName尚未创建任何章节', + 'profile_not_created_books' => ':userName尚未创建任何图书', + + /** + * Comments + */ + 'comment' => '评论', + 'comments' => '评论', + 'comment_placeholder' => '在这里评论', + 'comment_count' => '{0} 无评论|[1,*] :count条评论', + 'comment_save' => '保存评论', + 'comment_saving' => '正在保存评论...', + 'comment_deleting' => '正在删除评论...', + 'comment_new' => '新评论', + 'comment_created' => '评论于 :createDiff', + 'comment_updated' => '更新于 :updateDiff (:username)', + 'comment_deleted_success' => '评论已删除', + 'comment_created_success' => '评论已添加', + 'comment_updated_success' => '评论已更新', + 'comment_delete_confirm' => '你确定要删除这条评论?', + 'comment_in_reply_to' => '回复 :commentId', +]; diff --git a/resources/lang/zh_CN/errors.php b/resources/lang/zh_CN/errors.php new file mode 100644 index 000000000..1a5ee4fb6 --- /dev/null +++ b/resources/lang/zh_CN/errors.php @@ -0,0 +1,79 @@ + '您无权访问所请求的页面。', + 'permissionJson' => '您无权执行所请求的操作。', + + // Auth + 'error_user_exists_different_creds' => 'Email为 :email 的用户已经存在,但具有不同的凭据。', + 'email_already_confirmed' => 'Email已被确认,请尝试登录。', + 'email_confirmation_invalid' => '此确认令牌无效或已被使用,请重新注册。', + 'email_confirmation_expired' => '确认令牌已过期,已发送新的确认电子邮件。', + 'ldap_fail_anonymous' => '使用匿名绑定的LDAP访问失败。', + 'ldap_fail_authed' => '带有标识名称和密码的LDAP访问失败。', + 'ldap_extension_not_installed' => '未安装LDAP PHP扩展程序', + 'ldap_cannot_connect' => '无法连接到ldap服务器,初始连接失败', + 'social_no_action_defined' => '没有定义行为', + 'social_login_bad_response' => "在 :socialAccount 登录时遇到错误:\n:error", + 'social_account_in_use' => ':socialAccount 账户已被使用,请尝试通过 :socialAccount 选项登录。', + 'social_account_email_in_use' => 'Email :email 已经被使用。如果您已有帐户,则可以在个人资料设置中绑定您的 :socialAccount。', + 'social_account_existing' => ':socialAccount已经被绑定到您的账户。', + 'social_account_already_used_existing' => ':socialAccount账户已经被其他用户使用。', + 'social_account_not_used' => ':socialAccount账户没有绑定到任何用户,请在您的个人资料设置中绑定。', + 'social_account_register_instructions' => '如果您还没有帐户,您可以使用 :socialAccount 选项注册账户。', + 'social_driver_not_found' => '未找到社交驱动程序', + 'social_driver_not_configured' => '您的:socialAccount社交设置不正确。', + + // System + 'path_not_writable' => '无法上传到文件路径“:filePath”,请确保它可写入服务器。', + 'cannot_get_image_from_url' => '无法从 :url 中获取图片', + 'cannot_create_thumbs' => '服务器无法创建缩略图,请检查您是否安装了GD PHP扩展。', + 'server_upload_limit' => '上传图片时发生错误。', + 'image_upload_error' => '上传图片时发生错误', + + // Attachments + 'attachment_page_mismatch' => '附件更新期间的页面不匹配', + + // Pages + 'page_draft_autosave_fail' => '无法保存草稿,确保您在保存页面之前已经连接到互联网', + 'page_custom_home_deletion' => '无法删除一个被设置为主页的页面', + + // Entities + 'entity_not_found' => '未找到实体', + 'book_not_found' => '未找到图书', + 'page_not_found' => '未找到页面', + 'chapter_not_found' => '未找到章节', + 'selected_book_not_found' => '选中的书未找到', + 'selected_book_chapter_not_found' => '未找到所选的图书或章节', + 'guests_cannot_save_drafts' => '访客不能保存草稿', + + // Users + 'users_cannot_delete_only_admin' => '您不能删除唯一的管理员账户', + 'users_cannot_delete_guest' => '您不能删除访客用户', + + // Roles + 'role_cannot_be_edited' => '无法编辑该角色', + 'role_system_cannot_be_deleted' => '无法删除系统角色', + 'role_registration_default_cannot_delete' => '无法删除设置为默认注册的角色', + + // Comments + 'comment_list' => '提取评论时出现错误。', + 'cannot_add_comment_to_draft' => '您不能为草稿添加评论。', + 'comment_add' => '添加/更新评论时发生错误。', + 'comment_delete' => '删除评论时发生错误。', + 'empty_comment' => '不能添加空的评论。', + + // Error pages + '404_page_not_found' => '无法找到页面', + 'sorry_page_not_found' => '对不起,无法找到您想访问的页面。', + 'return_home' => '返回主页', + 'error_occurred' => '出现错误', + 'app_down' => ':appName现在正在关闭', + 'back_soon' => '请耐心等待网站的恢复。', +]; \ No newline at end of file diff --git a/resources/lang/zh_CN/pagination.php b/resources/lang/zh_CN/pagination.php new file mode 100644 index 000000000..f1fc4b5ae --- /dev/null +++ b/resources/lang/zh_CN/pagination.php @@ -0,0 +1,19 @@ + '« 上一页', + 'next' => '下一页 »', + +]; diff --git a/resources/lang/zh_CN/passwords.php b/resources/lang/zh_CN/passwords.php new file mode 100644 index 000000000..d4ba50c49 --- /dev/null +++ b/resources/lang/zh_CN/passwords.php @@ -0,0 +1,22 @@ + '密码必须至少包含六个字符并与确认相符。', + 'user' => "使用该Email地址的用户不存在。", + 'token' => '此密码重置令牌无效。', + 'sent' => '我们已经通过Email发送您的密码重置链接!', + 'reset' => '您的密码已被重置!', + +]; diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php new file mode 100755 index 000000000..9b731777f --- /dev/null +++ b/resources/lang/zh_CN/settings.php @@ -0,0 +1,117 @@ + '设置', + 'settings_save' => '保存设置', + 'settings_save_success' => '设置已保存', + + /** + * App settings + */ + + 'app_settings' => 'App设置', + 'app_name' => 'App名', + 'app_name_desc' => '此名称将在网页头部和Email中显示。', + 'app_name_header' => '在网页头部显示应用名?', + 'app_public_viewing' => '允许公众查看?', + 'app_secure_images' => '启用更高安全性的图片上传?', + 'app_secure_images_desc' => '出于性能原因,所有图像都是公开的。这个选项会在图像的网址前添加一个随机的,难以猜测的字符串,从而使直接访问变得困难。', + 'app_editor' => '页面编辑器', + 'app_editor_desc' => '选择所有用户将使用哪个编辑器来编辑页面。', + 'app_custom_html' => '自定义HTML头部内容', + 'app_custom_html_desc' => '此处添加的任何内容都将插入到每个页面的部分的底部,这对于覆盖样式或添加分析代码很方便。', + 'app_logo' => 'App Logo', + 'app_logo_desc' => '这个图片的高度应该为43px。
大图片将会被缩小。', + 'app_primary_color' => 'App主色', + 'app_primary_color_desc' => '这应该是一个十六进制值。
保留为空以重置为默认颜色。', + 'app_homepage' => 'App主页', + 'app_homepage_desc' => '选择要在主页上显示的页面来替换默认的视图,选定页面的访问权限将被忽略。', + 'app_homepage_default' => '默认主页视图选择', + 'app_disable_comments' => '禁用评论', + 'app_disable_comments_desc' => '在App的所有页面上禁用评论,现有评论也不会显示出来。', + + /** + * Registration settings + */ + + 'reg_settings' => '注册设置', + 'reg_allow' => '允许注册?', + 'reg_default_role' => '注册后的默认用户角色', + 'reg_confirm_email' => '需要Email验证?', + 'reg_confirm_email_desc' => '如果使用域名限制,则需要Email验证,并且该值将被忽略。', + 'reg_confirm_restrict_domain' => '域名限制', + 'reg_confirm_restrict_domain_desc' => '输入您想要限制注册的Email域名列表,用逗号隔开。在被允许与应用程序交互之前,用户将被发送一封Email来确认他们的地址。
注意用户在注册成功后可以修改他们的Email地址。', + 'reg_confirm_restrict_domain_placeholder' => '尚未设置限制', + + /** + * Role settings + */ + + 'roles' => '角色', + 'role_user_roles' => '用户角色', + 'role_create' => '创建角色', + 'role_create_success' => '角色创建成功', + 'role_delete' => '删除角色', + 'role_delete_confirm' => '这将会删除名为 \':roleName\' 的角色.', + 'role_delete_users_assigned' => '有:userCount位用户属于此角色。如果您想将此角色中的用户迁移,请在下面选择一个新角色。', + 'role_delete_no_migration' => "不要迁移用户", + 'role_delete_sure' => '您确定要删除这个角色?', + 'role_delete_success' => '角色删除成功', + 'role_edit' => '编辑角色', + 'role_details' => '角色详细信息', + 'role_name' => '角色名', + 'role_desc' => '角色简述', + 'role_system' => '系统权限', + 'role_manage_users' => '管理用户', + 'role_manage_roles' => '管理角色与角色权限', + 'role_manage_entity_permissions' => '管理所有图书,章节和页面的权限', + 'role_manage_own_entity_permissions' => '管理自己的图书,章节和页面的权限', + 'role_manage_settings' => '管理App设置', + 'role_asset' => '资源许可', + 'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍,章节和页面上的权限将覆盖这里的权限设定。', + 'role_all' => '全部的', + 'role_own' => '拥有的', + 'role_controlled_by_asset' => '由其所在的资源来控制', + 'role_save' => '保存角色', + 'role_update_success' => '角色更新成功', + 'role_users' => '此角色的用户', + 'role_users_none' => '目前没有用户被分配到这个角色', + + /** + * Users + */ + + 'users' => '用户', + 'user_profile' => '用户资料', + 'users_add_new' => '添加用户', + 'users_search' => '搜索用户', + 'users_role' => '用户角色', + 'users_external_auth_id' => '外部身份认证ID', + 'users_password_warning' => '如果您想更改密码,请填写以下内容:', + 'users_system_public' => '此用户代表访问您的App的任何访客。它不能用于登录,而是自动分配。', + 'users_books_view_type' => '图书浏览布局偏好', + 'users_delete' => '删除用户', + 'users_delete_named' => '删除用户 :userName', + 'users_delete_warning' => '这将从系统中完全删除名为 \':userName\' 的用户。', + 'users_delete_confirm' => '您确定要删除这个用户?', + 'users_delete_success' => '用户删除成功。', + 'users_edit' => '编辑用户', + 'users_edit_profile' => '编辑资料', + 'users_edit_success' => '用户更新成功', + 'users_avatar' => '用户头像', + 'users_avatar_desc' => '当前图片应该为约256px的正方形。', + 'users_preferred_language' => '语言', + 'users_social_accounts' => '社交账户', + 'users_social_accounts_info' => '在这里,您可以绑定您的其他帐户,以便更快更轻松地登录。如果您选择解除绑定,之后将不能通过此社交账户登录,请设置社交账户来取消本App的访问权限。', + 'users_social_connect' => '绑定账户', + 'users_social_disconnect' => '解除绑定账户', + 'users_social_connected' => ':socialAccount 账户已经成功绑定到您的资料。', + 'users_social_disconnected' => ':socialAccount 账户已经成功解除绑定。', +]; diff --git a/resources/lang/zh_CN/validation.php b/resources/lang/zh_CN/validation.php new file mode 100644 index 000000000..ae1ffd619 --- /dev/null +++ b/resources/lang/zh_CN/validation.php @@ -0,0 +1,108 @@ + ':attribute 需要被同意。', + 'active_url' => ':attribute 并不是一个有效的网址', + 'after' => ':attribute 必须是在 :date 后的日期。', + 'alpha' => ':attribute 只能包含字母。', + 'alpha_dash' => ':attribute 只能包含字母、数字和短横线。', + 'alpha_num' => ':attribute 只能包含字母和数字。', + 'array' => ':attribute 必须是一个数组。', + 'before' => ':attribute 必须是在 :date 前的日期。', + 'between' => [ + 'numeric' => ':attribute 必须在:min到:max之间。', + 'file' => ':attribute 必须为:min到:max KB。', + 'string' => ':attribute 必须在:min到:max个字符之间。', + 'array' => ':attribute 必须在:min到:max项之间.', + ], + 'boolean' => ':attribute 字段必须为真或假。', + 'confirmed' => ':attribute 确认不符。', + 'date' => ':attribute 不是一个有效的日期。', + 'date_format' => ':attribute 不匹配格式 :format。', + 'different' => ':attribute 和 :other 必须不同。', + 'digits' => ':attribute 必须为:digits位数。', + 'digits_between' => ':attribute 必须为:min到:max位数。', + 'email' => ':attribute 必须是有效的电子邮件地址。', + 'filled' => ':attribute 字段是必需的。', + 'exists' => '选中的 :attribute 无效。', + 'image' => ':attribute 必须是一个图片。', + 'in' => '选中的 :attribute 无效。', + 'integer' => ':attribute 必须是一个整数。', + 'ip' => ':attribute 必须是一个有效的IP地址。', + 'max' => [ + 'numeric' => ':attribute 不能超过:max。', + 'file' => ':attribute 不能超过:max KB。', + 'string' => ':attribute 不能超过:max个字符。', + 'array' => ':attribute 不能有超过:max项。', + ], + 'mimes' => ':attribute 必须是 :values 类型的文件。', + 'min' => [ + 'numeric' => ':attribute 至少为:min。', + 'file' => ':attribute 至少为:min KB。', + 'string' => ':attribute 至少为:min个字符。', + 'array' => ':attribute 至少有:min项。', + ], + 'not_in' => '选中的 :attribute 无效。', + 'numeric' => ':attribute 必须是一个数。', + 'regex' => ':attribute 格式无效。', + 'required' => ':attribute 字段是必需的。', + 'required_if' => '当:other为:value时,:attribute 字段是必需的。', + 'required_with' => '当:values存在时,:attribute 字段是必需的。', + 'required_with_all' => '当:values存在时,:attribute 字段是必需的。', + 'required_without' => '当:values不存在时,:attribute 字段是必需的。', + 'required_without_all' => '当:values均不存在时,:attribute 字段是必需的。', + 'same' => ':attribute 与 :other 必须匹配。', + 'size' => [ + 'numeric' => ':attribute 必须为:size。', + 'file' => ':attribute 必须为:size KB。', + 'string' => ':attribute 必须为:size个字符。', + 'array' => ':attribute 必须包含:size项。', + ], + 'string' => ':attribute 必须是字符串。', + 'timezone' => ':attribute 必须是有效的区域。', + 'unique' => ':attribute 已经被使用。', + 'url' => ':attribute 格式无效。', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'password-confirm' => [ + 'required_with' => '需要确认密码', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/views/books/grid-item.blade.php b/resources/views/books/grid-item.blade.php index cb2b447b0..db99d0b54 100644 --- a/resources/views/books/grid-item.blade.php +++ b/resources/views/books/grid-item.blade.php @@ -1,18 +1,18 @@ -
+
-
-

{{$book->getShortName(35)}}

+
+

{{$book->getShortName(35)}}

@if(isset($book->searchSnippet))

{!! $book->searchSnippet !!}

@else

{{ $book->getExcerpt(130) }}

@endif -
- @include('partials.entity-meta', ['entity' => $book]) -
+
+
\ No newline at end of file diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index d392af045..0d8a5fad9 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -1,8 +1,21 @@ @extends('sidebar-layout') @section('toolbar') -
-
+
+
+
id}/switch-book-view") }}" method="POST" class="inline"> + {!! csrf_field() !!} + {!! method_field('PATCH') !!} + + @if ($booksViewType === 'list') + + @else + + @endif +
+
+
+
@if($currentUser->can('book-create-all')) {{ trans('entities.books_create') }} @@ -52,15 +65,15 @@
@endforeach {!! $books->render() !!} - @else -
+ @else +
@foreach($books as $key => $book) @include('books/grid-item', ['book' => $book]) @endforeach -
+
+
{!! $books->render() !!}
-
@endif @else

{{ trans('entities.books_empty') }}

diff --git a/resources/views/books/list-item.blade.php b/resources/views/books/list-item.blade.php index 92d0f9e2d..a77ceee94 100644 --- a/resources/views/books/list-item.blade.php +++ b/resources/views/books/list-item.blade.php @@ -1,10 +1,10 @@
-

{{$book->name}}

+

{{$book->name}}

@if(isset($book->searchSnippet)) -

{!! $book->searchSnippet !!}

+

{!! $book->searchSnippet !!}

@else -

{{ $book->getExcerpt() }}

+

{{ $book->getExcerpt() }}

@endif
\ No newline at end of file diff --git a/resources/views/chapters/list-item.blade.php b/resources/views/chapters/list-item.blade.php index 9c1e2d640..3b1f84258 100644 --- a/resources/views/chapters/list-item.blade.php +++ b/resources/views/chapters/list-item.blade.php @@ -7,15 +7,15 @@   »   @endif - {{ $chapter->name }} + {{ $chapter->name }}
@if(isset($chapter->searchSnippet)) -

{!! $chapter->searchSnippet !!}

+

{!! $chapter->searchSnippet !!}

@else -

{{ $chapter->getExcerpt() }}

+

{{ $chapter->getExcerpt() }}

@endif
diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index 8f3b91435..8ad287bfc 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -99,7 +99,7 @@

{{ $chapter->name }}

-

{!! nl2br(e($chapter->description)) !!}

+

{!! nl2br(e($chapter->description)) !!}

@if(count($pages) > 0)
diff --git a/resources/views/components/code-editor.blade.php b/resources/views/components/code-editor.blade.php index 5788bd7f7..cd235f1d2 100644 --- a/resources/views/components/code-editor.blade.php +++ b/resources/views/components/code-editor.blade.php @@ -7,7 +7,7 @@
-
+