diff --git a/.env.example.complete b/.env.example.complete index 26df8f3cb..49d834ff7 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -281,6 +281,12 @@ ALLOW_CONTENT_SCRIPTS=false # Contents of the robots.txt file can be overridden, making this option obsolete. ALLOW_ROBOTS=null +# Allow server-side fetches to be performed to potentially unknown +# and user-provided locations. Primarily used in exports when loading +# in externally referenced assets. +# Can be 'true' or 'false'. +ALLOW_UNTRUSTED_SERVER_FETCHING=false + # A list of hosts that BookStack can be iframed within. # Space separated if multiple. BookStack host domain is auto-inferred. # For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com" diff --git a/.github/translators.txt b/.github/translators.txt index baaea5d92..cb6f466c9 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -167,3 +167,19 @@ whenwesober :: Indonesian Rem (remkovdhoef) :: Dutch syn7ax69 :: Bulgarian; Turkish Blaade :: French +Behzad HosseinPoor (behzad.hp) :: Persian +Ole Aldric (Swoy) :: Norwegian Bokmal +fharis arabia (raednahdi) :: Arabic +Alexander Predl (Harveyhase68) :: German +Rem (Rem9000) :: Dutch +Michał Stelmach (stelmach-web) :: Polish +arniom :: French +REMOVED_USER :: Turkish +林祖年 (contagion) :: Chinese Traditional +Siamak Guodarzi (siamakgoudarzi88) :: Persian +Lis Maestrelo (lismtrl) :: Portuguese, Brazilian +Nathanaël (nathanaelhoun) :: French +A Ibnu Hibban (abd.ibnuhibban) :: Indonesian +Frost-ZX :: Chinese Simplified +Kuzma Simonov (ovmach) :: Russian +Vojtěch Krystek (acantophis) :: Czech diff --git a/app/Actions/Activity.php b/app/Actions/Activity.php index c8590f0b2..6a8a9bcd0 100644 --- a/app/Actions/Activity.php +++ b/app/Actions/Activity.php @@ -11,16 +11,15 @@ use Illuminate\Support\Str; /** * @property string $type - * @property User $user + * @property User $user * @property Entity $entity * @property string $detail * @property string $entity_type - * @property int $entity_id - * @property int $user_id + * @property int $entity_id + * @property int $user_id */ class Activity extends Model { - /** * Get the entity for this activity. */ @@ -29,6 +28,7 @@ class Activity extends Model if ($this->entity_type === '') { $this->entity_type = null; } + return $this->morphTo('entity'); } @@ -54,7 +54,7 @@ class Activity extends Model public function isForEntity(): bool { return Str::startsWith($this->type, [ - 'page_', 'chapter_', 'book_', 'bookshelf_' + 'page_', 'chapter_', 'book_', 'bookshelf_', ]); } diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php index 73f827e70..dce7dc7b2 100644 --- a/app/Actions/ActivityService.php +++ b/app/Actions/ActivityService.php @@ -1,4 +1,6 @@ -activity->newInstance()->forceFill([ 'type' => strtolower($type), - 'user_id' => user()->id, + 'user_id' => user()->id, ]); } @@ -67,8 +70,8 @@ class ActivityService { $entity->activity()->update([ 'detail' => $entity->name, - 'entity_id' => null, - 'entity_type' => null, + 'entity_id' => null, + 'entity_type' => null, ]); } @@ -98,10 +101,10 @@ class ActivityService $queryIds = [$entity->getMorphClass() => [$entity->id]]; if ($entity->isA('book')) { - $queryIds[(new Chapter)->getMorphClass()] = $entity->chapters()->visible()->pluck('id'); + $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id'); } if ($entity->isA('book') || $entity->isA('chapter')) { - $queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id'); + $queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id'); } $query = $this->activity->newQuery(); @@ -143,7 +146,9 @@ class ActivityService /** * Filters out similar activity. + * * @param Activity[] $activities + * * @return array */ protected function filterSimilar(iterable $activities): array @@ -185,7 +190,7 @@ class ActivityService return; } - $message = str_replace("%u", $username, $message); + $message = str_replace('%u', $username, $message); $channel = config('logging.failed_login.channel'); Log::channel($channel)->warning($message); } diff --git a/app/Actions/ActivityType.php b/app/Actions/ActivityType.php index ec02bed25..60b1630e0 100644 --- a/app/Actions/ActivityType.php +++ b/app/Actions/ActivityType.php @@ -1,4 +1,6 @@ -comment = $comment; @@ -46,6 +46,7 @@ class CommentRepo $entity->comments()->save($comment); ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON); + return $comment; } @@ -58,6 +59,7 @@ class CommentRepo $comment->text = $text; $comment->html = $this->commentToHtml($text); $comment->save(); + return $comment; } @@ -75,8 +77,8 @@ class CommentRepo public function commentToHtml(string $commentText): string { $converter = new CommonMarkConverter([ - 'html_input' => 'strip', - 'max_nesting_level' => 10, + 'html_input' => 'strip', + 'max_nesting_level' => 10, 'allow_unsafe_links' => false, ]); @@ -89,6 +91,7 @@ class CommentRepo protected function getNextLocalId(Entity $entity): int { $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first(); + return ($comments->local_id ?? 0) + 1; } } diff --git a/app/Actions/Favourite.php b/app/Actions/Favourite.php index 107a76578..f45894182 100644 --- a/app/Actions/Favourite.php +++ b/app/Actions/Favourite.php @@ -1,4 +1,6 @@ -name) .'%5D'); + return url('/search?term=%5B' . urlencode($this->name) . '%5D'); } /** @@ -29,6 +31,6 @@ class Tag extends Model */ public function valueUrl(): string { - return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D'); + return url('/search?term=%5B' . urlencode($this->name) . '%3D' . urlencode($this->value) . '%5D'); } } diff --git a/app/Actions/TagRepo.php b/app/Actions/TagRepo.php index c80e8abe3..ca65b78e8 100644 --- a/app/Actions/TagRepo.php +++ b/app/Actions/TagRepo.php @@ -1,4 +1,6 @@ -permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); + return $query->get(['name'])->pluck('name'); } @@ -62,11 +64,12 @@ class TagRepo } $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); + return $query->get(['value'])->pluck('value'); } /** - * Save an array of tags to an entity + * Save an array of tags to an entity. */ public function saveTagsToEntity(Entity $entity, array $tags = []): iterable { @@ -89,6 +92,7 @@ class TagRepo { $name = trim($input['name']); $value = isset($input['value']) ? trim($input['value']) : ''; + return $this->tag->newInstance(['name' => $name, 'value' => $value]); } } diff --git a/app/Actions/View.php b/app/Actions/View.php index de30900c7..16961bd91 100644 --- a/app/Actions/View.php +++ b/app/Actions/View.php @@ -1,4 +1,6 @@ -generate(); Cache::put($cacheKey, $docs, 60 * 24); } + return $docs; } @@ -42,6 +44,7 @@ class ApiDocsGenerator $apiRoutes = $this->loadDetailsFromControllers($apiRoutes); $apiRoutes = $this->loadDetailsFromFiles($apiRoutes); $apiRoutes = $apiRoutes->groupBy('base_model'); + return $apiRoutes; } @@ -57,6 +60,7 @@ class ApiDocsGenerator $exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null; $route["example_{$exampleType}"] = $exampleContent; } + return $route; }); } @@ -71,12 +75,14 @@ class ApiDocsGenerator $comment = $method->getDocComment(); $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null; $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']); + return $route; }); } /** * Load body params and their rules by inspecting the given class and method name. + * * @throws BindingResolutionException */ protected function getBodyParamsFromClass(string $className, string $methodName): ?array @@ -92,6 +98,7 @@ class ApiDocsGenerator foreach ($rules as $param => $ruleString) { $rules[$param] = explode('|', $ruleString); } + return count($rules) > 0 ? $rules : null; } @@ -102,11 +109,13 @@ class ApiDocsGenerator { $matches = []; preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches); + return implode(' ', $matches[1] ?? []); } /** * Get a reflection method from the given class name and method name. + * * @throws ReflectionException */ protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod @@ -131,14 +140,15 @@ class ApiDocsGenerator [$controller, $controllerMethod] = explode('@', $route->action['uses']); $baseModelName = explode('.', explode('/', $route->uri)[1])[0]; $shortName = $baseModelName . '-' . $controllerMethod; + return [ - 'name' => $shortName, - 'uri' => $route->uri, - 'method' => $route->methods[0], - 'controller' => $controller, - 'controller_method' => $controllerMethod, + 'name' => $shortName, + 'uri' => $route->uri, + 'method' => $route->methods[0], + 'controller' => $controller, + 'controller_method' => $controllerMethod, 'controller_method_kebab' => Str::kebab($controllerMethod), - 'base_model' => $baseModelName, + 'base_model' => $baseModelName, ]; }); } diff --git a/app/Api/ApiToken.php b/app/Api/ApiToken.php index defaa7e95..f44fde19a 100644 --- a/app/Api/ApiToken.php +++ b/app/Api/ApiToken.php @@ -1,4 +1,6 @@ - 'date:Y-m-d' + 'expires_at' => 'date:Y-m-d', ]; /** diff --git a/app/Api/ApiTokenGuard.php b/app/Api/ApiTokenGuard.php index 59ab72f4e..8b9cbc8e1 100644 --- a/app/Api/ApiTokenGuard.php +++ b/app/Api/ApiTokenGuard.php @@ -2,6 +2,7 @@ namespace BookStack\Api; +use BookStack\Auth\Access\LoginService; use BookStack\Exceptions\ApiAuthException; use Illuminate\Auth\GuardHelpers; use Illuminate\Contracts\Auth\Authenticatable; @@ -12,7 +13,6 @@ use Symfony\Component\HttpFoundation\Request; class ApiTokenGuard implements Guard { - use GuardHelpers; /** @@ -20,9 +20,14 @@ class ApiTokenGuard implements Guard */ protected $request; + /** + * @var LoginService + */ + protected $loginService; /** * The last auth exception thrown in this request. + * * @var ApiAuthException */ protected $lastAuthException; @@ -30,11 +35,12 @@ class ApiTokenGuard implements Guard /** * ApiTokenGuard constructor. */ - public function __construct(Request $request) + public function __construct(Request $request, LoginService $loginService) { $this->request = $request; + $this->loginService = $loginService; } - + /** * @inheritDoc */ @@ -47,6 +53,7 @@ class ApiTokenGuard implements Guard } $user = null; + try { $user = $this->getAuthorisedUserFromRequest(); } catch (ApiAuthException $exception) { @@ -54,19 +61,20 @@ class ApiTokenGuard implements Guard } $this->user = $user; + return $user; } /** * Determine if current user is authenticated. If not, throw an exception. * - * @return \Illuminate\Contracts\Auth\Authenticatable - * * @throws ApiAuthException + * + * @return \Illuminate\Contracts\Auth\Authenticatable */ public function authenticate() { - if (! is_null($user = $this->user())) { + if (!is_null($user = $this->user())) { return $user; } @@ -79,6 +87,7 @@ class ApiTokenGuard implements Guard /** * Check the API token in the request and fetch a valid authorised user. + * * @throws ApiAuthException */ protected function getAuthorisedUserFromRequest(): Authenticatable @@ -93,11 +102,16 @@ class ApiTokenGuard implements Guard $this->validateToken($token, $secret); + if ($this->loginService->awaitingEmailConfirmation($token->user)) { + throw new ApiAuthException(trans('errors.email_confirmation_awaiting')); + } + return $token->user; } /** * Validate the format of the token header value string. + * * @throws ApiAuthException */ protected function validateTokenHeaderValue(string $authToken): void @@ -114,6 +128,7 @@ class ApiTokenGuard implements Guard /** * Validate the given secret against the given token and ensure the token * currently has access to the instance API. + * * @throws ApiAuthException */ protected function validateToken(?ApiToken $token, string $secret): void diff --git a/app/Api/ListingResponseBuilder.php b/app/Api/ListingResponseBuilder.php index df4cb8bf1..02b3f680c 100644 --- a/app/Api/ListingResponseBuilder.php +++ b/app/Api/ListingResponseBuilder.php @@ -1,4 +1,6 @@ - '<', 'gte' => '>=', 'lte' => '<=', - 'like' => 'like' + 'like' => 'like', ]; /** @@ -42,7 +43,7 @@ class ListingResponseBuilder $data = $this->fetchData($filteredQuery); return response()->json([ - 'data' => $data, + 'data' => $data, 'total' => $total, ]); } @@ -54,6 +55,7 @@ class ListingResponseBuilder { $query = $this->countAndOffsetQuery($query); $query = $this->sortQuery($query); + return $query->get($this->fields); } @@ -95,6 +97,7 @@ class ListingResponseBuilder } $queryOperator = $this->filterOperators[$filterOperator]; + return [$field, $queryOperator, $value]; } diff --git a/app/Application.php b/app/Application.php index 499fdeaa6..d409d14bc 100644 --- a/app/Application.php +++ b/app/Application.php @@ -4,11 +4,11 @@ namespace BookStack; class Application extends \Illuminate\Foundation\Application { - /** * Get the path to the application configuration files. * - * @param string $path Optionally, a path to append to the config path + * @param string $path Optionally, a path to append to the config path + * * @return string */ public function configPath($path = '') @@ -18,6 +18,6 @@ class Application extends \Illuminate\Foundation\Application . 'app' . DIRECTORY_SEPARATOR . 'Config' - . ($path ? DIRECTORY_SEPARATOR.$path : $path); + . ($path ? DIRECTORY_SEPARATOR . $path : $path); } } diff --git a/app/Auth/Access/EmailConfirmationService.php b/app/Auth/Access/EmailConfirmationService.php index 9aa3b9b98..9c357d95f 100644 --- a/app/Auth/Access/EmailConfirmationService.php +++ b/app/Auth/Access/EmailConfirmationService.php @@ -1,4 +1,6 @@ -display_name))); + return in_array($roleName, $groupNames); } @@ -57,7 +58,7 @@ class ExternalAuthService } /** - * Sync the groups to the user roles for the current user + * Sync the groups to the user roles for the current user. */ public function syncWithGroups(User $user, array $userGroups): void { diff --git a/app/Auth/Access/ExternalBaseUserProvider.php b/app/Auth/Access/ExternalBaseUserProvider.php index 69295ee4e..fde610c3e 100644 --- a/app/Auth/Access/ExternalBaseUserProvider.php +++ b/app/Auth/Access/ExternalBaseUserProvider.php @@ -7,7 +7,6 @@ use Illuminate\Contracts\Auth\UserProvider; class ExternalBaseUserProvider implements UserProvider { - /** * The user model. * @@ -17,7 +16,8 @@ class ExternalBaseUserProvider implements UserProvider /** * LdapUserProvider constructor. - * @param $model + * + * @param $model */ public function __construct(string $model) { @@ -32,13 +32,15 @@ class ExternalBaseUserProvider implements UserProvider public function createModel() { $class = '\\' . ltrim($this->model, '\\'); - return new $class; + + return new $class(); } /** * Retrieve a user by their unique identifier. * - * @param mixed $identifier + * @param mixed $identifier + * * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveById($identifier) @@ -49,8 +51,9 @@ class ExternalBaseUserProvider implements UserProvider /** * Retrieve a user by their unique identifier and "remember me" token. * - * @param mixed $identifier - * @param string $token + * @param mixed $identifier + * @param string $token + * * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByToken($identifier, $token) @@ -58,12 +61,12 @@ class ExternalBaseUserProvider implements UserProvider return null; } - /** * Update the "remember me" token for the given user in storage. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param string $token + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param string $token + * * @return void */ public function updateRememberToken(Authenticatable $user, $token) @@ -74,13 +77,15 @@ class ExternalBaseUserProvider implements UserProvider /** * Retrieve a user by the given credentials. * - * @param array $credentials + * @param array $credentials + * * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByCredentials(array $credentials) { // Search current user base by looking up a uid $model = $this->createModel(); + return $model->newQuery() ->where('external_auth_id', $credentials['external_auth_id']) ->first(); @@ -89,8 +94,9 @@ class ExternalBaseUserProvider implements UserProvider /** * Validate a user against the given credentials. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param array $credentials + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param array $credentials + * * @return bool */ public function validateCredentials(Authenticatable $user, array $credentials) diff --git a/app/Auth/Access/Guards/ExternalBaseSessionGuard.php b/app/Auth/Access/Guards/ExternalBaseSessionGuard.php index b133754d8..99bfd2e79 100644 --- a/app/Auth/Access/Guards/ExternalBaseSessionGuard.php +++ b/app/Auth/Access/Guards/ExternalBaseSessionGuard.php @@ -84,7 +84,7 @@ class ExternalBaseSessionGuard implements StatefulGuard // If we've already retrieved the user for the current request we can just // return it back immediately. We do not want to fetch the user data on // every call to this method because that would be tremendously slow. - if (! is_null($this->user)) { + if (!is_null($this->user)) { return $this->user; } @@ -92,7 +92,7 @@ class ExternalBaseSessionGuard implements StatefulGuard // First we will try to load the user using the // identifier in the session if one exists. - if (! is_null($id)) { + if (!is_null($id)) { $this->user = $this->provider->retrieveById($id); } @@ -118,7 +118,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log a user into the application without sessions or cookies. * - * @param array $credentials + * @param array $credentials + * * @return bool */ public function once(array $credentials = []) @@ -135,12 +136,13 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log the given user ID into the application without sessions or cookies. * - * @param mixed $id + * @param mixed $id + * * @return \Illuminate\Contracts\Auth\Authenticatable|false */ public function onceUsingId($id) { - if (! is_null($user = $this->provider->retrieveById($id))) { + if (!is_null($user = $this->provider->retrieveById($id))) { $this->setUser($user); return $user; @@ -152,7 +154,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Validate a user's credentials. * - * @param array $credentials + * @param array $credentials + * * @return bool */ public function validate(array $credentials = []) @@ -160,12 +163,12 @@ class ExternalBaseSessionGuard implements StatefulGuard return false; } - /** * Attempt to authenticate a user using the given credentials. * - * @param array $credentials - * @param bool $remember + * @param array $credentials + * @param bool $remember + * * @return bool */ public function attempt(array $credentials = [], $remember = false) @@ -176,26 +179,24 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log the given user ID into the application. * - * @param mixed $id - * @param bool $remember + * @param mixed $id + * @param bool $remember + * * @return \Illuminate\Contracts\Auth\Authenticatable|false */ public function loginUsingId($id, $remember = false) { - if (! is_null($user = $this->provider->retrieveById($id))) { - $this->login($user, $remember); - - return $user; - } - + // Always return false as to disable this method, + // Logins should route through LoginService. return false; } /** * Log a user into the application. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param bool $remember + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param bool $remember + * * @return void */ public function login(AuthenticatableContract $user, $remember = false) @@ -208,7 +209,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Update the session with the given ID. * - * @param string $id + * @param string $id + * * @return void */ protected function updateSession($id) @@ -262,7 +264,7 @@ class ExternalBaseSessionGuard implements StatefulGuard */ public function getName() { - return 'login_'.$this->name.'_'.sha1(static::class); + return 'login_' . $this->name . '_' . sha1(static::class); } /** @@ -288,7 +290,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Set the current user. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * * @return $this */ public function setUser(AuthenticatableContract $user) diff --git a/app/Auth/Access/Guards/LdapSessionGuard.php b/app/Auth/Access/Guards/LdapSessionGuard.php index 71417aed2..7f6965937 100644 --- a/app/Auth/Access/Guards/LdapSessionGuard.php +++ b/app/Auth/Access/Guards/LdapSessionGuard.php @@ -6,8 +6,8 @@ use BookStack\Auth\Access\LdapService; use BookStack\Auth\Access\RegistrationService; use BookStack\Auth\User; use BookStack\Exceptions\LdapException; -use BookStack\Exceptions\LoginAttemptException; use BookStack\Exceptions\LoginAttemptEmailNeededException; +use BookStack\Exceptions\LoginAttemptException; use BookStack\Exceptions\UserRegistrationException; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Session\Session; @@ -15,7 +15,6 @@ use Illuminate\Support\Str; class LdapSessionGuard extends ExternalBaseSessionGuard { - protected $ldapService; /** @@ -36,8 +35,10 @@ class LdapSessionGuard extends ExternalBaseSessionGuard * Validate a user's credentials. * * @param array $credentials - * @return bool + * * @throws LdapException + * + * @return bool */ public function validate(array $credentials = []) { @@ -45,7 +46,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard if (isset($userDetails['uid'])) { $this->lastAttempted = $this->provider->retrieveByCredentials([ - 'external_auth_id' => $userDetails['uid'] + 'external_auth_id' => $userDetails['uid'], ]); } @@ -56,10 +57,12 @@ class LdapSessionGuard extends ExternalBaseSessionGuard * Attempt to authenticate a user using the given credentials. * * @param array $credentials - * @param bool $remember - * @return bool + * @param bool $remember + * * @throws LoginAttemptException * @throws LdapException + * + * @return bool */ public function attempt(array $credentials = [], $remember = false) { @@ -69,7 +72,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard $user = null; if (isset($userDetails['uid'])) { $this->lastAttempted = $user = $this->provider->retrieveByCredentials([ - 'external_auth_id' => $userDetails['uid'] + 'external_auth_id' => $userDetails['uid'], ]); } @@ -96,11 +99,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard } $this->login($user, $remember); + return true; } /** - * Create a new user from the given ldap credentials and login credentials + * Create a new user from the given ldap credentials and login credentials. + * * @throws LoginAttemptEmailNeededException * @throws LoginAttemptException * @throws UserRegistrationException @@ -114,14 +119,15 @@ class LdapSessionGuard extends ExternalBaseSessionGuard } $details = [ - 'name' => $ldapUserDetails['name'], - 'email' => $ldapUserDetails['email'] ?: $credentials['email'], + 'name' => $ldapUserDetails['name'], + 'email' => $ldapUserDetails['email'] ?: $credentials['email'], 'external_auth_id' => $ldapUserDetails['uid'], - 'password' => Str::random(32), + 'password' => Str::random(32), ]; $user = $this->registrationService->registerUser($details, null, false); $this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails); + return $user; } } diff --git a/app/Auth/Access/Guards/Saml2SessionGuard.php b/app/Auth/Access/Guards/Saml2SessionGuard.php index 044c2f383..eacd5d21e 100644 --- a/app/Auth/Access/Guards/Saml2SessionGuard.php +++ b/app/Auth/Access/Guards/Saml2SessionGuard.php @@ -3,7 +3,7 @@ namespace BookStack\Auth\Access\Guards; /** - * Saml2 Session Guard + * Saml2 Session Guard. * * The saml2 login process is async in nature meaning it does not fit very well * into the default laravel 'Guard' auth flow. Instead most of the logic is done @@ -16,6 +16,7 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard * Validate a user's credentials. * * @param array $credentials + * * @return bool */ public function validate(array $credentials = []) @@ -27,7 +28,8 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard * Attempt to authenticate a user using the given credentials. * * @param array $credentials - * @param bool $remember + * @param bool $remember + * * @return bool */ public function attempt(array $credentials = [], $remember = false) diff --git a/app/Auth/Access/Ldap.php b/app/Auth/Access/Ldap.php index 352231df5..b5c70e498 100644 --- a/app/Auth/Access/Ldap.php +++ b/app/Auth/Access/Ldap.php @@ -1,4 +1,6 @@ -search($ldapConnection, $baseDn, $filter, $attributes); + return $this->getEntries($ldapConnection, $search); } /** * Bind to LDAP directory. + * * @param resource $ldapConnection * @param string $bindRdn * @param string $bindPassword + * * @return bool */ public function bind($ldapConnection, $bindRdn = null, $bindPassword = null) @@ -102,8 +118,10 @@ class Ldap /** * Explode a LDAP dn string into an array of components. + * * @param string $dn - * @param int $withAttrib + * @param int $withAttrib + * * @return array */ public function explodeDn(string $dn, int $withAttrib) @@ -113,12 +131,14 @@ class Ldap /** * Escape a string for use in an LDAP filter. + * * @param string $value * @param string $ignore - * @param int $flags + * @param int $flags + * * @return string */ - public function escape(string $value, string $ignore = "", int $flags = 0) + public function escape(string $value, string $ignore = '', int $flags = 0) { return ldap_escape($value, $ignore, $flags); } diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index 2f632b0b5..7bfdb5328 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -1,4 +1,6 @@ -getUserResponseProperty($user, 'cn', null); $formatted = [ - 'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']), - 'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn), - 'dn' => $user['dn'], + 'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']), + 'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn), + 'dn' => $user['dn'], 'email' => $this->getUserResponseProperty($user, $emailAttr, null), 'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null, ]; if ($this->config['dump_user_details']) { throw new JsonDebugException([ - 'details_from_ldap' => $user, + 'details_from_ldap' => $user, 'details_bookstack_parsed' => $formatted, ]); } @@ -137,6 +141,7 @@ class LdapService extends ExternalAuthService /** * Check if the given credentials are valid for the given user. + * * @throws LdapException */ public function validateUserCredentials(?array $ldapUserDetails, string $password): bool @@ -146,6 +151,7 @@ class LdapService extends ExternalAuthService } $ldapConnection = $this->getConnection(); + try { $ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password); } catch (ErrorException $e) { @@ -158,7 +164,9 @@ class LdapService extends ExternalAuthService /** * Bind the system user to the LDAP connection using the given credentials * otherwise anonymous access is attempted. + * * @param $connection + * * @throws LdapException */ protected function bindSystemUser($connection) @@ -181,8 +189,10 @@ class LdapService extends ExternalAuthService /** * Get the connection to the LDAP server. * Creates a new connection if one does not exist. - * @return resource + * * @throws LdapException + * + * @return resource */ protected function getConnection() { @@ -222,6 +232,7 @@ class LdapService extends ExternalAuthService } $this->ldapConnection = $ldapConnection; + return $this->ldapConnection; } @@ -241,6 +252,7 @@ class LdapService extends ExternalAuthService // Otherwise, extract the port out $hostName = $serverNameParts[0]; $ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389; + return ['host' => $hostName, 'port' => $ldapPort]; } @@ -254,11 +266,13 @@ class LdapService extends ExternalAuthService $newKey = '${' . $key . '}'; $newAttrs[$newKey] = $this->ldap->escape($attrText); } + return strtr($filterString, $newAttrs); } /** * Get the groups a user is a part of on ldap. + * * @throws LdapException */ public function getUserGroups(string $userName): array @@ -272,11 +286,13 @@ class LdapService extends ExternalAuthService $userGroups = $this->groupFilter($user); $userGroups = $this->getGroupsRecursive($userGroups, []); + return $userGroups; } /** * Get the parent groups of an array of groups. + * * @throws LdapException */ private function getGroupsRecursive(array $groupsArray, array $checked): array @@ -303,6 +319,7 @@ class LdapService extends ExternalAuthService /** * Get the parent groups of a single group. + * * @throws LdapException */ private function getGroupGroups(string $groupName): array @@ -336,7 +353,7 @@ class LdapService extends ExternalAuthService $count = 0; if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { - $count = (int)$userGroupSearchResponse[$groupsAttr]['count']; + $count = (int) $userGroupSearchResponse[$groupsAttr]['count']; } for ($i = 0; $i < $count; $i++) { @@ -351,6 +368,7 @@ class LdapService extends ExternalAuthService /** * Sync the LDAP groups to the user roles for the current user. + * * @throws LdapException */ public function syncGroups(User $user, string $username) diff --git a/app/Auth/Access/LoginService.php b/app/Auth/Access/LoginService.php new file mode 100644 index 000000000..e02296b37 --- /dev/null +++ b/app/Auth/Access/LoginService.php @@ -0,0 +1,164 @@ +mfaSession = $mfaSession; + $this->emailConfirmationService = $emailConfirmationService; + } + + /** + * Log the given user into the system. + * Will start a login of the given user but will prevent if there's + * a reason to (MFA or Unconfirmed Email). + * Returns a boolean to indicate the current login result. + * + * @throws StoppedAuthenticationException + */ + public function login(User $user, string $method, bool $remember = false): void + { + if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) { + $this->setLastLoginAttemptedForUser($user, $method, $remember); + + throw new StoppedAuthenticationException($user, $this); + } + + $this->clearLastLoginAttempted(); + auth()->login($user, $remember); + Activity::add(ActivityType::AUTH_LOGIN, "{$method}; {$user->logDescriptor()}"); + Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user); + + // Authenticate on all session guards if a likely admin + if ($user->can('users-manage') && $user->can('user-roles-manage')) { + $guards = ['standard', 'ldap', 'saml2']; + foreach ($guards as $guard) { + auth($guard)->login($user); + } + } + } + + /** + * Reattempt a system login after a previous stopped attempt. + * + * @throws Exception + */ + public function reattemptLoginFor(User $user) + { + if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) { + throw new Exception('Login reattempt user does align with current session state'); + } + + $lastLoginDetails = $this->getLastLoginAttemptDetails(); + $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false); + } + + /** + * Get the last user that was attempted to be logged in. + * Only exists if the last login attempt had correct credentials + * but had been prevented by a secondary factor. + */ + public function getLastLoginAttemptUser(): ?User + { + $id = $this->getLastLoginAttemptDetails()['user_id']; + + return User::query()->where('id', '=', $id)->first(); + } + + /** + * Get the details of the last login attempt. + * Checks upon a ttl of about 1 hour since that last attempted login. + * + * @return array{user_id: ?string, method: ?string, remember: bool} + */ + protected function getLastLoginAttemptDetails(): array + { + $value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY); + if (!$value) { + return ['user_id' => null, 'method' => null]; + } + + [$id, $method, $remember, $time] = explode(':', $value); + $hourAgo = time() - (60 * 60); + if ($time < $hourAgo) { + $this->clearLastLoginAttempted(); + + return ['user_id' => null, 'method' => null]; + } + + return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)]; + } + + /** + * Set the last login attempted user. + * Must be only used when credentials are correct and a login could be + * achieved but a secondary factor has stopped the login. + */ + protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember) + { + session()->put( + self::LAST_LOGIN_ATTEMPTED_SESSION_KEY, + implode(':', [$user->id, $method, $remember, time()]) + ); + } + + /** + * Clear the last login attempted session value. + */ + protected function clearLastLoginAttempted(): void + { + session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY); + } + + /** + * Check if MFA verification is needed. + */ + public function needsMfaVerification(User $user): bool + { + return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user); + } + + /** + * Check if the given user is awaiting email confirmation. + */ + public function awaitingEmailConfirmation(User $user): bool + { + return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed; + } + + /** + * Attempt the login of a user using the given credentials. + * Meant to mirror Laravel's default guard 'attempt' method + * but in a manner that always routes through our login system. + * May interrupt the flow if extra authentication requirements are imposed. + * + * @throws StoppedAuthenticationException + */ + public function attempt(array $credentials, string $method, bool $remember = false): bool + { + $result = auth()->attempt($credentials, $remember); + if ($result) { + $user = auth()->user(); + auth()->logout(); + $this->login($user, $method, $remember); + } + + return $result; + } +} diff --git a/app/Auth/Access/Mfa/BackupCodeService.php b/app/Auth/Access/Mfa/BackupCodeService.php new file mode 100644 index 000000000..d58d28ae1 --- /dev/null +++ b/app/Auth/Access/Mfa/BackupCodeService.php @@ -0,0 +1,62 @@ +cleanInputCode($code); + $codes = json_decode($codeSet); + + return in_array($cleanCode, $codes); + } + + /** + * Remove the given input code from the given available options. + * Will return a JSON string containing the codes. + */ + public function removeInputCodeFromSet(string $code, string $codeSet): string + { + $cleanCode = $this->cleanInputCode($code); + $codes = json_decode($codeSet); + $pos = array_search($cleanCode, $codes, true); + array_splice($codes, $pos, 1); + + return json_encode($codes); + } + + /** + * Count the number of codes in the given set. + */ + public function countCodesInSet(string $codeSet): int + { + return count(json_decode($codeSet)); + } + + protected function cleanInputCode(string $code): string + { + return strtolower(str_replace(' ', '-', trim($code))); + } +} diff --git a/app/Auth/Access/Mfa/MfaSession.php b/app/Auth/Access/Mfa/MfaSession.php new file mode 100644 index 000000000..72163b58e --- /dev/null +++ b/app/Auth/Access/Mfa/MfaSession.php @@ -0,0 +1,60 @@ +mfaValues()->exists() || $this->userRoleEnforcesMfa($user); + } + + /** + * Check if the given user is pending MFA setup. + * (MFA required but not yet configured). + */ + public function isPendingMfaSetup(User $user): bool + { + return $this->isRequiredForUser($user) && !$user->mfaValues()->exists(); + } + + /** + * Check if a role of the given user enforces MFA. + */ + protected function userRoleEnforcesMfa(User $user): bool + { + return $user->roles() + ->where('mfa_enforced', '=', true) + ->exists(); + } + + /** + * Check if the current MFA session has already been verified for the given user. + */ + public function isVerifiedForUser(User $user): bool + { + return session()->get($this->getMfaVerifiedSessionKey($user)) === 'true'; + } + + /** + * Mark the current session as MFA-verified. + */ + public function markVerifiedForUser(User $user): void + { + session()->put($this->getMfaVerifiedSessionKey($user), 'true'); + } + + /** + * Get the session key in which the MFA verification status is stored. + */ + protected function getMfaVerifiedSessionKey(User $user): string + { + return 'mfa-verification-passed:' . $user->id; + } +} diff --git a/app/Auth/Access/Mfa/MfaValue.php b/app/Auth/Access/Mfa/MfaValue.php new file mode 100644 index 000000000..8f07c6657 --- /dev/null +++ b/app/Auth/Access/Mfa/MfaValue.php @@ -0,0 +1,76 @@ +firstOrNew([ + 'user_id' => $user->id, + 'method' => $method, + ]); + $mfaVal->setValue($value); + $mfaVal->save(); + } + + /** + * Easily get the decrypted MFA value for the given user and method. + */ + public static function getValueForUser(User $user, string $method): ?string + { + /** @var MfaValue $mfaVal */ + $mfaVal = static::query() + ->where('user_id', '=', $user->id) + ->where('method', '=', $method) + ->first(); + + return $mfaVal ? $mfaVal->getValue() : null; + } + + /** + * Decrypt the value attribute upon access. + */ + protected function getValue(): string + { + return decrypt($this->value); + } + + /** + * Encrypt the value attribute upon access. + */ + protected function setValue($value): void + { + $this->value = encrypt($value); + } +} diff --git a/app/Auth/Access/Mfa/TotpService.php b/app/Auth/Access/Mfa/TotpService.php new file mode 100644 index 000000000..a3e9fc827 --- /dev/null +++ b/app/Auth/Access/Mfa/TotpService.php @@ -0,0 +1,72 @@ +google2fa = $google2fa; + // Use SHA1 as a default, Personal testing of other options in 2021 found + // many apps lack support for other algorithms yet still will scan + // the code causing a confusing UX. + $this->google2fa->setAlgorithm(Constants::SHA1); + } + + /** + * Generate a new totp secret key. + */ + public function generateSecret(): string + { + /** @noinspection PhpUnhandledExceptionInspection */ + return $this->google2fa->generateSecretKey(); + } + + /** + * Generate a TOTP URL from secret key. + */ + public function generateUrl(string $secret): string + { + return $this->google2fa->getQRCodeUrl( + setting('app-name'), + user()->email, + $secret + ); + } + + /** + * Generate a QR code to display a TOTP URL. + */ + public function generateQrCodeSvg(string $url): string + { + $color = Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(32, 110, 167)); + + return (new Writer( + new ImageRenderer( + new RendererStyle(192, 0, null, null, $color), + new SvgImageBackEnd() + ) + ))->writeString($url); + } + + /** + * Verify that the user provided code is valid for the secret. + * The secret must be known, not user-provided. + */ + public function verifyCode(string $code, string $secret): bool + { + /** @noinspection PhpUnhandledExceptionInspection */ + return $this->google2fa->verifyKey($secret, $code); + } +} diff --git a/app/Auth/Access/Mfa/TotpValidationRule.php b/app/Auth/Access/Mfa/TotpValidationRule.php new file mode 100644 index 000000000..22cb7da9b --- /dev/null +++ b/app/Auth/Access/Mfa/TotpValidationRule.php @@ -0,0 +1,37 @@ +secret = $secret; + $this->totpService = app()->make(TotpService::class); + } + + /** + * Determine if the validation rule passes. + */ + public function passes($attribute, $value) + { + return $this->totpService->verifyCode($value, $this->secret); + } + + /** + * Get the validation error message. + */ + public function message() + { + return trans('validation.totp'); + } +} diff --git a/app/Auth/Access/RegistrationService.php b/app/Auth/Access/RegistrationService.php index 68b17771d..16e3edbb4 100644 --- a/app/Auth/Access/RegistrationService.php +++ b/app/Auth/Access/RegistrationService.php @@ -1,4 +1,6 @@ -flash('sent-email-confirmation', true); } catch (Exception $e) { $message = trans('auth.email_confirm_send_error'); + throw new UserRegistrationException($message, '/register/confirm'); } } @@ -94,6 +99,7 @@ class RegistrationService /** * Ensure that the given email meets any active email domain registration restrictions. * Throws if restrictions are active and the email does not match an allowed domain. + * * @throws UserRegistrationException */ protected function ensureEmailDomainAllowed(string $userEmail): void @@ -105,9 +111,10 @@ class RegistrationService } $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict)); - $userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, "@"), 1); + $userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, '@'), 1); if (!in_array($userEmailDomain, $restrictedEmailDomains)) { $redirect = $this->registrationAllowed() ? '/register' : '/login'; + throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect); } } diff --git a/app/Auth/Access/Saml2Service.php b/app/Auth/Access/Saml2Service.php index 105853997..6cbfdac0b 100644 --- a/app/Auth/Access/Saml2Service.php +++ b/app/Auth/Access/Saml2Service.php @@ -1,13 +1,12 @@ -config = config('saml2'); $this->registrationService = $registrationService; - $this->user = $user; + $this->loginService = $loginService; } /** * Initiate a login flow. + * * @throws Error */ public function login(): array { $toolKit = $this->getToolkit(); $returnRoute = url('/saml2/acs'); + return [ 'url' => $toolKit->login($returnRoute, [], false, false, true), - 'id' => $toolKit->getLastRequestID(), + 'id' => $toolKit->getLastRequestID(), ]; } /** * Initiate a logout flow. + * * @throws Error */ public function logout(): array @@ -78,6 +80,7 @@ class Saml2Service extends ExternalAuthService * Process the ACS response from the idp and return the * matching, or new if registration active, user matched to the idp. * Returns null if not authenticated. + * * @throws Error * @throws SamlException * @throws ValidationError @@ -92,7 +95,7 @@ class Saml2Service extends ExternalAuthService if (!empty($errors)) { throw new Error( - 'Invalid ACS Response: '.implode(', ', $errors) + 'Invalid ACS Response: ' . implode(', ', $errors) ); } @@ -108,6 +111,7 @@ class Saml2Service extends ExternalAuthService /** * Process a response for the single logout service. + * * @throws Error */ public function processSlsResponse(?string $requestId): ?string @@ -119,11 +123,12 @@ class Saml2Service extends ExternalAuthService if (!empty($errors)) { throw new Error( - 'Invalid SLS Response: '.implode(', ', $errors) + 'Invalid SLS Response: ' . implode(', ', $errors) ); } $this->actionLogout(); + return $redirect; } @@ -138,6 +143,7 @@ class Saml2Service extends ExternalAuthService /** * Get the metadata for this service provider. + * * @throws Error */ public function metadata(): string @@ -149,7 +155,7 @@ class Saml2Service extends ExternalAuthService if (!empty($errors)) { throw new Error( - 'Invalid SP metadata: '.implode(', ', $errors), + 'Invalid SP metadata: ' . implode(', ', $errors), Error::METADATA_SP_INVALID ); } @@ -159,6 +165,7 @@ class Saml2Service extends ExternalAuthService /** * Load the underlying Onelogin SAML2 toolkit. + * * @throws Error * @throws Exception */ @@ -178,6 +185,7 @@ class Saml2Service extends ExternalAuthService $spSettings = $this->loadOneloginServiceProviderDetails(); $settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides); + return new Auth($settings); } @@ -187,18 +195,18 @@ class Saml2Service extends ExternalAuthService protected function loadOneloginServiceProviderDetails(): array { $spDetails = [ - 'entityId' => url('/saml2/metadata'), + 'entityId' => url('/saml2/metadata'), 'assertionConsumerService' => [ 'url' => url('/saml2/acs'), ], 'singleLogoutService' => [ - 'url' => url('/saml2/sls') + 'url' => url('/saml2/sls'), ], ]; return [ 'baseurl' => url('/saml2'), - 'sp' => $spDetails + 'sp' => $spDetails, ]; } @@ -211,7 +219,7 @@ class Saml2Service extends ExternalAuthService } /** - * Calculate the display name + * Calculate the display name. */ protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string { @@ -261,9 +269,9 @@ class Saml2Service extends ExternalAuthService return [ 'external_id' => $externalId, - 'name' => $this->getUserDisplayName($samlAttributes, $externalId), - 'email' => $email, - 'saml_id' => $samlID, + 'name' => $this->getUserDisplayName($samlAttributes, $externalId), + 'email' => $email, + 'saml_id' => $samlID, ]; } @@ -297,6 +305,7 @@ class Saml2Service extends ExternalAuthService $data = $data[0]; break; } + return $data; } @@ -315,19 +324,20 @@ class Saml2Service extends ExternalAuthService /** * Get the user from the database for the specified details. + * * @throws UserRegistrationException */ protected function getOrRegisterUser(array $userDetails): ?User { - $user = $this->user->newQuery() + $user = User::query() ->where('external_auth_id', '=', $userDetails['external_id']) ->first(); if (is_null($user)) { $userData = [ - 'name' => $userDetails['name'], - 'email' => $userDetails['email'], - 'password' => Str::random(32), + 'name' => $userDetails['name'], + 'email' => $userDetails['email'], + 'password' => Str::random(32), 'external_auth_id' => $userDetails['external_id'], ]; @@ -340,9 +350,11 @@ class Saml2Service extends ExternalAuthService /** * Process the SAML response for a user. Login the user when * they exist, optionally registering them automatically. + * * @throws SamlException * @throws JsonDebugException * @throws UserRegistrationException + * @throws StoppedAuthenticationException */ public function processLoginCallback(string $samlID, array $samlAttributes): User { @@ -351,8 +363,8 @@ class Saml2Service extends ExternalAuthService if ($this->config['dump_user_details']) { throw new JsonDebugException([ - 'id_from_idp' => $samlID, - 'attrs_from_idp' => $samlAttributes, + 'id_from_idp' => $samlID, + 'attrs_from_idp' => $samlAttributes, 'attrs_after_parsing' => $userDetails, ]); } @@ -375,9 +387,8 @@ class Saml2Service extends ExternalAuthService $this->syncWithGroups($user, $groups); } - auth()->login($user); - Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}"); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user); + $this->loginService->login($user, 'saml2'); + return $user; } } diff --git a/app/Auth/Access/SocialAuthService.php b/app/Auth/Access/SocialAuthService.php index a03eb2b1d..8cf243fe7 100644 --- a/app/Auth/Access/SocialAuthService.php +++ b/app/Auth/Access/SocialAuthService.php @@ -1,14 +1,12 @@ - */ protected $configureForRedirectCallbacks = []; @@ -54,33 +60,39 @@ class SocialAuthService /** * SocialAuthService constructor. */ - public function __construct(Socialite $socialite) + public function __construct(Socialite $socialite, LoginService $loginService) { $this->socialite = $socialite; + $this->loginService = $loginService; } /** * Start the social login path. + * * @throws SocialDriverNotConfigured */ public function startLogIn(string $socialDriver): RedirectResponse { $driver = $this->validateDriver($socialDriver); + return $this->getDriverForRedirect($driver)->redirect(); } /** - * Start the social registration process + * Start the social registration process. + * * @throws SocialDriverNotConfigured */ public function startRegister(string $socialDriver): RedirectResponse { $driver = $this->validateDriver($socialDriver); + return $this->getDriverForRedirect($driver)->redirect(); } /** * Handle the social registration process on callback. + * * @throws UserRegistrationException */ public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser @@ -92,6 +104,7 @@ class SocialAuthService if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) { $email = $socialUser->getEmail(); + throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login'); } @@ -100,16 +113,19 @@ class SocialAuthService /** * Get the social user details via the social driver. + * * @throws SocialDriverNotConfigured */ public function getSocialUser(string $socialDriver): SocialUser { $driver = $this->validateDriver($socialDriver); + return $this->socialite->driver($driver)->user(); } /** * Handle the login process on a oAuth callback. + * * @throws SocialSignInAccountNotUsed */ public function handleLoginCallback(string $socialDriver, SocialUser $socialUser) @@ -125,9 +141,8 @@ class SocialAuthService // When a user is not logged in and a matching SocialAccount exists, // Simply log the user into the application. if (!$isLoggedIn && $socialAccount !== null) { - auth()->login($socialAccount->user); - Activity::add(ActivityType::AUTH_LOGIN, $socialAccount); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user); + $this->loginService->login($socialAccount->user, $socialAccount); + return redirect()->intended('/'); } @@ -137,18 +152,21 @@ class SocialAuthService $account = $this->newSocialAccount($socialDriver, $socialUser); $currentUser->socialAccounts()->save($account); session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver])); + return redirect($currentUser->getEditUrl()); } // When a user is logged in and the social account exists and is already linked to the current user. if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver])); + return redirect($currentUser->getEditUrl()); } // When a user is logged in, A social account exists but the users do not match. if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver])); + return redirect($currentUser->getEditUrl()); } @@ -163,6 +181,7 @@ class SocialAuthService /** * Ensure the social driver is correct and supported. + * * @throws SocialDriverNotConfigured */ protected function validateDriver(string $socialDriver): string @@ -188,6 +207,7 @@ class SocialAuthService $lowerName = strtolower($driver); $configPrefix = 'services.' . $lowerName . '.'; $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')]; + return !in_array(false, $config) && !in_array(null, $config); } @@ -237,9 +257,9 @@ class SocialAuthService public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount { return new SocialAccount([ - 'driver' => $socialDriver, + 'driver' => $socialDriver, 'driver_id' => $socialUser->getId(), - 'avatar' => $socialUser->getAvatar() + 'avatar' => $socialUser->getAvatar(), ]); } @@ -252,7 +272,7 @@ class SocialAuthService } /** - * Provide redirect options per service for the Laravel Socialite driver + * Provide redirect options per service for the Laravel Socialite driver. */ protected function getDriverForRedirect(string $driverName): Provider { diff --git a/app/Auth/Access/UserInviteService.php b/app/Auth/Access/UserInviteService.php index 20519fc7d..d884cd636 100644 --- a/app/Auth/Access/UserInviteService.php +++ b/app/Auth/Access/UserInviteService.php @@ -1,4 +1,6 @@ -db = $db; - } - /** * Delete all email confirmations that belong to a user. + * * @param User $user + * * @return mixed */ public function deleteByUser(User $user) { - return $this->db->table($this->tokenTable) + return DB::table($this->tokenTable) ->where('user_id', '=', $user->id) ->delete(); } /** * Get the user id from a token, while check the token exists and has not expired. + * * @param string $token - * @return int + * * @throws UserTokenNotFoundException * @throws UserTokenExpiredException + * + * @return int */ - public function checkTokenAndGetUserId(string $token) : int + public function checkTokenAndGetUserId(string $token): int { $entry = $this->getEntryByToken($token); @@ -70,63 +67,74 @@ class UserTokenService /** * Creates a unique token within the email confirmation database. + * * @return string */ - protected function generateToken() : string + protected function generateToken(): string { $token = Str::random(24); while ($this->tokenExists($token)) { $token = Str::random(25); } + return $token; } /** * Generate and store a token for the given user. + * * @param User $user + * * @return string */ - protected function createTokenForUser(User $user) : string + protected function createTokenForUser(User $user): string { $token = $this->generateToken(); - $this->db->table($this->tokenTable)->insert([ - 'user_id' => $user->id, - 'token' => $token, + DB::table($this->tokenTable)->insert([ + 'user_id' => $user->id, + 'token' => $token, 'created_at' => Carbon::now(), - 'updated_at' => Carbon::now() + 'updated_at' => Carbon::now(), ]); + return $token; } /** * Check if the given token exists. + * * @param string $token + * * @return bool */ - protected function tokenExists(string $token) : bool + protected function tokenExists(string $token): bool { - return $this->db->table($this->tokenTable) + return DB::table($this->tokenTable) ->where('token', '=', $token)->exists(); } /** * Get a token entry for the given token. + * * @param string $token + * * @return object|null */ protected function getEntryByToken(string $token) { - return $this->db->table($this->tokenTable) + return DB::table($this->tokenTable) ->where('token', '=', $token) ->first(); } /** * Check if the given token entry has expired. + * * @param stdClass $tokenEntry + * * @return bool */ - protected function entryExpired(stdClass $tokenEntry) : bool + protected function entryExpired(stdClass $tokenEntry): bool { return Carbon::now()->subHours($this->expiryTime) ->gt(new Carbon($tokenEntry->created_at)); diff --git a/app/Auth/Permissions/EntityPermission.php b/app/Auth/Permissions/EntityPermission.php index ef61e03ce..131771a38 100644 --- a/app/Auth/Permissions/EntityPermission.php +++ b/app/Auth/Permissions/EntityPermission.php @@ -1,15 +1,17 @@ - function ($query) { $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']); - } + }, ]); } /** * Build joint permissions for the given shelf and role combinations. + * * @throws Throwable */ protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false) @@ -169,6 +173,7 @@ class PermissionService /** * Build joint permissions for the given book and role combinations. + * * @throws Throwable */ protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false) @@ -193,6 +198,7 @@ class PermissionService /** * Rebuild the entity jointPermissions for a particular entity. + * * @throws Throwable */ public function buildJointPermissionsForEntity(Entity $entity) @@ -201,6 +207,7 @@ class PermissionService if ($entity instanceof Book) { $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get(); $this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true); + return; } @@ -224,6 +231,7 @@ class PermissionService /** * Rebuild the entity jointPermissions for a collection of entities. + * * @throws Throwable */ public function buildJointPermissionsForEntities(array $entities) @@ -263,6 +271,7 @@ class PermissionService /** * Delete all of the entity jointPermissions for a list of entities. + * * @param Role[] $roles */ protected function deleteManyJointPermissionsForRoles($roles) @@ -275,7 +284,9 @@ class PermissionService /** * Delete the entity jointPermissions for a particular entity. + * * @param Entity $entity + * * @throws Throwable */ public function deleteJointPermissionsForEntity(Entity $entity) @@ -285,7 +296,9 @@ class PermissionService /** * Delete all of the entity jointPermissions for a list of entities. + * * @param Entity[] $entities + * * @throws Throwable */ protected function deleteManyJointPermissionsForEntities(array $entities) @@ -295,7 +308,6 @@ class PermissionService } $this->db->transaction(function () use ($entities) { - foreach (array_chunk($entities, 1000) as $entityChunk) { $query = $this->db->table('joint_permissions'); foreach ($entityChunk as $entity) { @@ -311,8 +323,10 @@ class PermissionService /** * Create & Save entity jointPermissions for many entities and roles. + * * @param Entity[] $entities - * @param Role[] $roles + * @param Role[] $roles + * * @throws Throwable */ protected function createManyJointPermissions(array $entities, array $roles) @@ -363,7 +377,6 @@ class PermissionService }); } - /** * Get the actions related to an entity. */ @@ -376,6 +389,7 @@ class PermissionService if ($entity instanceof Book) { $baseActions[] = 'chapter-create'; } + return $baseActions; } @@ -397,6 +411,7 @@ class PermissionService if ($entity->restricted) { $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction); + return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); } @@ -433,6 +448,7 @@ class PermissionService protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool { $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action; + return $entityMap[$key] ?? false; } @@ -443,18 +459,19 @@ class PermissionService protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array { return [ - 'role_id' => $role->getRawAttribute('id'), - 'entity_id' => $entity->getRawAttribute('id'), - 'entity_type' => $entity->getMorphClass(), - 'action' => $action, - 'has_permission' => $permissionAll, + 'role_id' => $role->getRawAttribute('id'), + 'entity_id' => $entity->getRawAttribute('id'), + 'entity_type' => $entity->getMorphClass(), + 'action' => $action, + 'has_permission' => $permissionAll, 'has_permission_own' => $permissionOwn, - 'owned_by' => $entity->getRawAttribute('owned_by'), + 'owned_by' => $entity->getRawAttribute('owned_by'), ]; } /** * Checks if an entity has a restriction set upon it. + * * @param HasCreatorAndUpdater|HasOwner $ownable */ public function checkOwnableUserAccess(Model $ownable, string $permission): bool @@ -473,7 +490,8 @@ class PermissionService $ownPermission = $user && $user->can($permission . '-own'); $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by'; $isOwner = $user && $user->id === $ownable->$ownerField; - return ($allPermission || ($isOwner && $ownPermission)); + + return $allPermission || ($isOwner && $ownPermission); } // Handle abnormal create jointPermissions @@ -483,6 +501,7 @@ class PermissionService $hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0; $this->clean(); + return $hasAccess; } @@ -509,6 +528,7 @@ class PermissionService $hasPermission = $permissionQuery->count() > 0; $this->clean(); + return $hasPermission; } @@ -529,6 +549,7 @@ class PermissionService }); $this->clean(); + return $q; } @@ -539,6 +560,7 @@ class PermissionService public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder { $this->clean(); + return $query->where(function (Builder $parentQuery) use ($ability) { $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) { $permissionQuery->whereIn('role_id', $this->getCurrentUserRoles()) @@ -580,6 +602,7 @@ class PermissionService /** * Filter items that have entities set as a polymorphic relation. + * * @param Builder|\Illuminate\Database\Query\Builder $query */ public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view') @@ -600,6 +623,7 @@ class PermissionService }); $this->clean(); + return $q; } @@ -628,12 +652,14 @@ class PermissionService }); $this->clean(); + return $q; } /** * Add the query for checking the given user id has permission * within the join_permissions table. + * * @param QueryBuilder|Builder $query */ protected function addJointHasPermissionCheck($query, int $userIdToCheck) @@ -645,7 +671,7 @@ class PermissionService } /** - * Get the current user + * Get the current user. */ private function currentUser(): User { diff --git a/app/Auth/Permissions/PermissionsRepo.php b/app/Auth/Permissions/PermissionsRepo.php index f54612a43..988146700 100644 --- a/app/Auth/Permissions/PermissionsRepo.php +++ b/app/Auth/Permissions/PermissionsRepo.php @@ -1,4 +1,6 @@ -role->newInstance($roleData); + $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->save(); $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $this->assignRolePermissions($role, $permissions); $this->permissionService->buildJointPermissionForRole($role); Activity::add(ActivityType::ROLE_CREATE, $role); + return $role; } @@ -88,6 +91,7 @@ class PermissionsRepo $this->assignRolePermissions($role, $permissions); $role->fill($roleData); + $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->save(); $this->permissionService->buildJointPermissionForRole($role); Activity::add(ActivityType::ROLE_UPDATE, $role); @@ -116,6 +120,7 @@ class PermissionsRepo * Check it's not an admin role or set as default before deleting. * If an migration Role ID is specified the users assign to the current role * will be added to the role of the specified id. + * * @throws PermissionsException * @throws Exception */ @@ -127,7 +132,7 @@ class PermissionsRepo // Prevent deleting admin role or default registration role. if ($role->system_name && in_array($role->system_name, $this->systemRoles)) { throw new PermissionsException(trans('errors.role_system_cannot_be_deleted')); - } else if ($role->id === intval(setting('registration-role'))) { + } elseif ($role->id === intval(setting('registration-role'))) { throw new PermissionsException(trans('errors.role_registration_default_cannot_delete')); } diff --git a/app/Auth/Permissions/RolePermission.php b/app/Auth/Permissions/RolePermission.php index 7f44ff815..0a0e6ff17 100644 --- a/app/Auth/Permissions/RolePermission.php +++ b/app/Auth/Permissions/RolePermission.php @@ -1,4 +1,6 @@ -where('system_name', '=', 'public')->first(); + return static::$defaultUser; } @@ -98,13 +111,15 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * The roles that belong to the user. + * * @return BelongsToMany */ public function roles() { if ($this->id === 0) { - return ; + return; } + return $this->belongsToMany(Role::class); } @@ -194,7 +209,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Check if the user has a social account, * If a driver is passed it checks for that single account type. + * * @param bool|string $socialDriver + * * @return bool */ public function hasSocialAccount($socialDriver = false) @@ -207,7 +224,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } /** - * Returns a URL to the user's avatar + * Returns a URL to the user's avatar. */ public function getAvatar(int $size = 50): string { @@ -222,6 +239,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } catch (Exception $err) { $avatar = $default; } + return $avatar; } @@ -249,6 +267,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->hasMany(Favourite::class); } + /** + * Get the MFA values belonging to this use. + */ + public function mfaValues(): HasMany + { + return $this->hasMany(MfaValue::class); + } + /** * Get the last activity time for this user. */ @@ -268,6 +294,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getEditUrl(string $path = ''): string { $uri = '/settings/users/' . $this->id . '/' . trim($path, '/'); + return url(rtrim($uri, '/')); } @@ -298,7 +325,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Send the password reset notification. - * @param string $token + * + * @param string $token + * * @return void */ public function sendPasswordResetNotification($token) @@ -320,6 +349,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function refreshSlug(): string { $this->slug = app(SlugGenerator::class)->generate($this); + return $this->slug; } } diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 15ce2cbc9..e1a040fc2 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -1,4 +1,6 @@ -select(['*']) ->withLastActivityAt() ->with(['roles', 'avatar']) + ->withCount('mfaValues') ->orderBy($sort, $sortData['order']); if ($sortData['search']) { @@ -82,7 +85,7 @@ class UserRepo return $query->paginate($count); } - /** + /** * Creates a new user and attaches a role to them. */ public function registerNew(array $data, bool $emailConfirmed = false): User @@ -96,6 +99,7 @@ class UserRepo /** * Assign a user to a system-level role. + * * @throws NotFoundException */ public function attachSystemRole(User $user, string $systemRoleName) @@ -126,6 +130,7 @@ class UserRepo /** * Set the assigned user roles via an array of role IDs. + * * @throws UserUpdateException */ public function setUserRoles(User $user, array $roles) @@ -141,7 +146,7 @@ class UserRepo * Check if the given user is the last admin and their new roles no longer * contains the admin role. */ - protected function demotingLastAdmin(User $user, array $newRoles) : bool + protected function demotingLastAdmin(User $user, array $newRoles): bool { if ($this->isOnlyAdmin($user)) { $adminRole = Role::getSystemRole('admin'); @@ -159,10 +164,10 @@ class UserRepo public function create(array $data, bool $emailConfirmed = false): User { $details = [ - 'name' => $data['name'], - 'email' => $data['email'], - 'password' => bcrypt($data['password']), - 'email_confirmed' => $emailConfirmed, + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => bcrypt($data['password']), + 'email_confirmed' => $emailConfirmed, 'external_auth_id' => $data['external_auth_id'] ?? '', ]; @@ -176,6 +181,7 @@ class UserRepo /** * Remove the given user from storage, Delete all related content. + * * @throws Exception */ public function destroy(User $user, ?int $newOwnerId = null) @@ -183,8 +189,9 @@ class UserRepo $user->socialAccounts()->delete(); $user->apiTokens()->delete(); $user->favourites()->delete(); + $user->mfaValues()->delete(); $user->delete(); - + // Delete user profile images $this->userAvatar->destroyAllForUser($user); @@ -201,7 +208,7 @@ class UserRepo */ protected function migrateOwnership(User $fromUser, User $toUser) { - $entities = (new EntityProvider)->all(); + $entities = (new EntityProvider())->all(); foreach ($entities as $instance) { $instance->newQuery()->where('owned_by', '=', $fromUser->id) ->update(['owned_by' => $toUser->id]); @@ -242,11 +249,12 @@ class UserRepo public function getAssetCounts(User $user): array { $createdBy = ['created_by' => $user->id]; + return [ - 'pages' => Page::visible()->where($createdBy)->count(), - 'chapters' => Chapter::visible()->where($createdBy)->count(), - 'books' => Book::visible()->where($createdBy)->count(), - 'shelves' => Bookshelf::visible()->where($createdBy)->count(), + 'pages' => Page::visible()->where($createdBy)->count(), + 'chapters' => Chapter::visible()->where($createdBy)->count(), + 'books' => Book::visible()->where($createdBy)->count(), + 'shelves' => Bookshelf::visible()->where($createdBy)->count(), ]; } diff --git a/app/Config/api.php b/app/Config/api.php index 6afea2dc8..03f191fee 100644 --- a/app/Config/api.php +++ b/app/Config/api.php @@ -18,6 +18,6 @@ return [ 'max_item_count' => env('API_MAX_ITEM_COUNT', 500), // The number of API requests that can be made per minute by a single user. - 'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180) + 'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180), ]; diff --git a/app/Config/app.php b/app/Config/app.php index 550c98b0a..120644aed 100755 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -36,6 +36,11 @@ return [ // Even when overridden the WYSIWYG editor may still escape script content. 'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false), + // Allow server-side fetches to be performed to potentially unknown + // and user-provided locations. Primarily used in exports when loading + // in externally referenced assets. + 'allow_untrusted_server_fetching' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false), + // Override the default behaviour for allowing crawlers to crawl the instance. // May be ignored if view has be overridden or modified. // Defaults to null since, if not set, 'app-public' status used instead. @@ -56,7 +61,7 @@ return [ 'locale' => env('APP_LANG', 'en'), // Locales available - 'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',], + 'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'], // Application Fallback Locale 'fallback_locale' => 'en', @@ -140,52 +145,52 @@ return [ 'aliases' => [ // Laravel - 'App' => Illuminate\Support\Facades\App::class, - 'Arr' => Illuminate\Support\Arr::class, - 'Artisan' => Illuminate\Support\Facades\Artisan::class, - 'Auth' => Illuminate\Support\Facades\Auth::class, - 'Blade' => Illuminate\Support\Facades\Blade::class, - 'Bus' => Illuminate\Support\Facades\Bus::class, - 'Cache' => Illuminate\Support\Facades\Cache::class, - 'Config' => Illuminate\Support\Facades\Config::class, - 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Crypt' => Illuminate\Support\Facades\Crypt::class, - 'DB' => Illuminate\Support\Facades\DB::class, - 'Eloquent' => Illuminate\Database\Eloquent\Model::class, - 'Event' => Illuminate\Support\Facades\Event::class, - 'File' => Illuminate\Support\Facades\File::class, - 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Input' => Illuminate\Support\Facades\Input::class, - 'Inspiring' => Illuminate\Foundation\Inspiring::class, - 'Lang' => Illuminate\Support\Facades\Lang::class, - 'Log' => Illuminate\Support\Facades\Log::class, - 'Mail' => Illuminate\Support\Facades\Mail::class, + 'App' => Illuminate\Support\Facades\App::class, + 'Arr' => Illuminate\Support\Arr::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Input' => Illuminate\Support\Facades\Input::class, + 'Inspiring' => Illuminate\Foundation\Inspiring::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, 'Notification' => Illuminate\Support\Facades\Notification::class, - 'Password' => Illuminate\Support\Facades\Password::class, - 'Queue' => Illuminate\Support\Facades\Queue::class, - 'Redirect' => Illuminate\Support\Facades\Redirect::class, - 'Redis' => Illuminate\Support\Facades\Redis::class, - 'Request' => Illuminate\Support\Facades\Request::class, - 'Response' => Illuminate\Support\Facades\Response::class, - 'Route' => Illuminate\Support\Facades\Route::class, - 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Session' => Illuminate\Support\Facades\Session::class, - 'Storage' => Illuminate\Support\Facades\Storage::class, - 'Str' => Illuminate\Support\Str::class, - 'URL' => Illuminate\Support\Facades\URL::class, - 'Validator' => Illuminate\Support\Facades\Validator::class, - 'View' => Illuminate\Support\Facades\View::class, - 'Socialite' => Laravel\Socialite\Facades\Socialite::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'Str' => Illuminate\Support\Str::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + 'Socialite' => Laravel\Socialite\Facades\Socialite::class, // Third Party 'ImageTool' => Intervention\Image\Facades\Image::class, - 'DomPDF' => Barryvdh\DomPDF\Facade::class, + 'DomPDF' => Barryvdh\DomPDF\Facade::class, 'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class, // Custom BookStack - 'Activity' => BookStack\Facades\Activity::class, + 'Activity' => BookStack\Facades\Activity::class, 'Permissions' => BookStack\Facades\Permissions::class, - 'Theme' => BookStack\Facades\Theme::class, + 'Theme' => BookStack\Facades\Theme::class, ], // Proxy configuration diff --git a/app/Config/auth.php b/app/Config/auth.php index 51b152ff1..404b5352d 100644 --- a/app/Config/auth.php +++ b/app/Config/auth.php @@ -18,7 +18,7 @@ return [ // This option controls the default authentication "guard" and password // reset options for your application. 'defaults' => [ - 'guard' => env('AUTH_METHOD', 'standard'), + 'guard' => env('AUTH_METHOD', 'standard'), 'passwords' => 'users', ], @@ -29,15 +29,15 @@ return [ // Supported drivers: "session", "api-token", "ldap-session" 'guards' => [ 'standard' => [ - 'driver' => 'session', + 'driver' => 'session', 'provider' => 'users', ], 'ldap' => [ - 'driver' => 'ldap-session', + 'driver' => 'ldap-session', 'provider' => 'external', ], 'saml2' => [ - 'driver' => 'saml2-session', + 'driver' => 'saml2-session', 'provider' => 'external', ], 'api' => [ @@ -52,11 +52,11 @@ return [ 'providers' => [ 'users' => [ 'driver' => 'eloquent', - 'model' => \BookStack\Auth\User::class, + 'model' => \BookStack\Auth\User::class, ], 'external' => [ 'driver' => 'external-users', - 'model' => \BookStack\Auth\User::class, + 'model' => \BookStack\Auth\User::class, ], ], @@ -67,9 +67,9 @@ return [ 'passwords' => [ 'users' => [ 'provider' => 'users', - 'email' => 'emails.password', - 'table' => 'password_resets', - 'expire' => 60, + 'email' => 'emails.password', + 'table' => 'password_resets', + 'expire' => 60, ], ], diff --git a/app/Config/broadcasting.php b/app/Config/broadcasting.php index 7aaaa5693..5e929d373 100644 --- a/app/Config/broadcasting.php +++ b/app/Config/broadcasting.php @@ -23,18 +23,18 @@ return [ 'connections' => [ 'pusher' => [ - 'driver' => 'pusher', - 'key' => env('PUSHER_APP_KEY'), - 'secret' => env('PUSHER_APP_SECRET'), - 'app_id' => env('PUSHER_APP_ID'), + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'secret' => env('PUSHER_APP_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), 'options' => [ 'cluster' => env('PUSHER_APP_CLUSTER'), - 'useTLS' => true, + 'useTLS' => true, ], ], 'redis' => [ - 'driver' => 'redis', + 'driver' => 'redis', 'connection' => 'default', ], @@ -46,7 +46,6 @@ return [ 'driver' => 'null', ], - ], ]; diff --git a/app/Config/cache.php b/app/Config/cache.php index 33d3a1a0b..f9b7ed1d2 100644 --- a/app/Config/cache.php +++ b/app/Config/cache.php @@ -42,8 +42,8 @@ return [ ], 'database' => [ - 'driver' => 'database', - 'table' => 'cache', + 'driver' => 'database', + 'table' => 'cache', 'connection' => null, ], @@ -58,7 +58,7 @@ return [ ], 'redis' => [ - 'driver' => 'redis', + 'driver' => 'redis', 'connection' => 'default', ], diff --git a/app/Config/database.php b/app/Config/database.php index 170666ddb..7fb51a13b 100644 --- a/app/Config/database.php +++ b/app/Config/database.php @@ -59,38 +59,38 @@ return [ 'connections' => [ 'mysql' => [ - 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), - 'host' => $mysql_host, - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'unix_socket' => env('DB_SOCKET', ''), - 'port' => $mysql_port, - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => $mysql_host, + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'port' => $mysql_port, + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', 'prefix_indexes' => true, - 'strict' => false, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ + 'strict' => false, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], 'mysql_testing' => [ - 'driver' => 'mysql', - 'url' => env('TEST_DATABASE_URL'), - 'host' => '127.0.0.1', - 'database' => 'bookstack-test', - 'username' => env('MYSQL_USER', 'bookstack-test'), - 'password' => env('MYSQL_PASSWORD', 'bookstack-test'), - 'port' => $mysql_port, - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', + 'driver' => 'mysql', + 'url' => env('TEST_DATABASE_URL'), + 'host' => '127.0.0.1', + 'database' => 'bookstack-test', + 'username' => env('MYSQL_USER', 'bookstack-test'), + 'password' => env('MYSQL_PASSWORD', 'bookstack-test'), + 'port' => $mysql_port, + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', 'prefix_indexes' => true, - 'strict' => false, + 'strict' => false, ], ], diff --git a/app/Config/debugbar.php b/app/Config/debugbar.php index fe624eb7d..53b5a0872 100644 --- a/app/Config/debugbar.php +++ b/app/Config/debugbar.php @@ -1,7 +1,7 @@ env('DEBUGBAR_ENABLED', false), - 'except' => [ - 'telescope*' + 'except' => [ + 'telescope*', ], - - // DebugBar stores data for session/ajax requests. - // You can disable this, so the debugbar stores data in headers/session, - // but this can cause problems with large data collectors. - // By default, file storage (in the storage folder) is used. Redis and PDO - // can also be used. For PDO, run the package migrations first. + // DebugBar stores data for session/ajax requests. + // You can disable this, so the debugbar stores data in headers/session, + // but this can cause problems with large data collectors. + // By default, file storage (in the storage folder) is used. Redis and PDO + // can also be used. For PDO, run the package migrations first. 'storage' => [ 'enabled' => true, 'driver' => 'file', // redis, file, pdo, custom 'path' => storage_path('debugbar'), // For file driver 'connection' => null, // Leave null for default connection (Redis/PDO) - 'provider' => '' // Instance of StorageInterface for custom driver + 'provider' => '', // Instance of StorageInterface for custom driver ], - // Vendor files are included by default, but can be set to false. - // This can also be set to 'js' or 'css', to only include javascript or css vendor files. - // Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) - // and for js: jquery and and highlight.js - // So if you want syntax highlighting, set it to true. - // jQuery is set to not conflict with existing jQuery scripts. + // Vendor files are included by default, but can be set to false. + // This can also be set to 'js' or 'css', to only include javascript or css vendor files. + // Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) + // and for js: jquery and and highlight.js + // So if you want syntax highlighting, set it to true. + // jQuery is set to not conflict with existing jQuery scripts. 'include_vendors' => true, - // The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), - // you can use this option to disable sending the data through the headers. - // Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. + // The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), + // you can use this option to disable sending the data through the headers. + // Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. - 'capture_ajax' => true, + 'capture_ajax' => true, 'add_ajax_timing' => false, - // When enabled, the Debugbar shows deprecated warnings for Symfony components - // in the Messages tab. + // When enabled, the Debugbar shows deprecated warnings for Symfony components + // in the Messages tab. 'error_handler' => false, - // The Debugbar can emulate the Clockwork headers, so you can use the Chrome - // Extension, without the server-side code. It uses Debugbar collectors instead. + // The Debugbar can emulate the Clockwork headers, so you can use the Chrome + // Extension, without the server-side code. It uses Debugbar collectors instead. 'clockwork' => false, - // Enable/disable DataCollectors + // Enable/disable DataCollectors 'collectors' => [ 'phpinfo' => true, // Php version 'messages' => true, // Messages @@ -82,7 +81,7 @@ return [ 'models' => true, // Display models ], - // Configure some DataCollectors + // Configure some DataCollectors 'options' => [ 'auth' => [ 'show_name' => true, // Also show the users name/email in the debugbar @@ -91,43 +90,43 @@ return [ 'with_params' => true, // Render SQL with the parameters substituted 'backtrace' => true, // Use a backtrace to find the origin of the query in your files. 'timeline' => false, // Add the queries to the timeline - 'explain' => [ // Show EXPLAIN output on queries + 'explain' => [ // Show EXPLAIN output on queries 'enabled' => false, - 'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ + 'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ ], 'hints' => true, // Show hints for common mistakes ], 'mail' => [ - 'full_log' => false + 'full_log' => false, ], 'views' => [ 'data' => false, //Note: Can slow down the application, because the data can be quite large.. ], 'route' => [ - 'label' => true // show complete route on bar + 'label' => true, // show complete route on bar ], 'logs' => [ - 'file' => null + 'file' => null, ], 'cache' => [ - 'values' => true // collect cache values + 'values' => true, // collect cache values ], ], - // Inject Debugbar into the response - // Usually, the debugbar is added just before , by listening to the - // Response after the App is done. If you disable this, you have to add them - // in your template yourself. See http://phpdebugbar.com/docs/rendering.html + // Inject Debugbar into the response + // Usually, the debugbar is added just before , by listening to the + // Response after the App is done. If you disable this, you have to add them + // in your template yourself. See http://phpdebugbar.com/docs/rendering.html 'inject' => true, - // DebugBar route prefix - // Sometimes you want to set route prefix to be used by DebugBar to load - // its resources from. Usually the need comes from misconfigured web server or - // from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 + // DebugBar route prefix + // Sometimes you want to set route prefix to be used by DebugBar to load + // its resources from. Usually the need comes from misconfigured web server or + // from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 'route_prefix' => '_debugbar', - // DebugBar route domain - // By default DebugBar route served from the same domain that request served. - // To override default domain, specify it as a non-empty value. + // DebugBar route domain + // By default DebugBar route served from the same domain that request served. + // To override default domain, specify it as a non-empty value. 'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''), ]; diff --git a/app/Config/dompdf.php b/app/Config/dompdf.php index 094739cd9..cf07312e8 100644 --- a/app/Config/dompdf.php +++ b/app/Config/dompdf.php @@ -10,12 +10,11 @@ return [ - 'show_warnings' => false, // Throw an Exception on warnings from dompdf - 'orientation' => 'portrait', - 'defines' => [ + 'orientation' => 'portrait', + 'defines' => [ /** - * The location of the DOMPDF font directory + * The location of the DOMPDF font directory. * * The location of the directory where DOMPDF will store fonts and font metrics * Note: This directory must exist and be writable by the webserver process. @@ -38,17 +37,17 @@ return [ * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, * Symbol, ZapfDingbats. */ - "DOMPDF_FONT_DIR" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) + 'font_dir' => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) /** - * The location of the DOMPDF font cache directory + * The location of the DOMPDF font cache directory. * * This directory contains the cached font metrics for the fonts used by DOMPDF. * This directory can be the same as DOMPDF_FONT_DIR * * Note: This directory must exist and be writable by the webserver process. */ - "DOMPDF_FONT_CACHE" => storage_path('fonts/'), + 'font_cache' => storage_path('fonts/'), /** * The location of a temporary directory. @@ -57,10 +56,10 @@ return [ * The temporary directory is required to download remote images and when * using the PFDLib back end. */ - "DOMPDF_TEMP_DIR" => sys_get_temp_dir(), + 'temp_dir' => sys_get_temp_dir(), /** - * ==== IMPORTANT ==== + * ==== IMPORTANT ====. * * dompdf's "chroot": Prevents dompdf from accessing system files or other * files on the webserver. All local files opened by dompdf must be in a @@ -71,7 +70,7 @@ return [ * direct class use like: * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output(); */ - "DOMPDF_CHROOT" => realpath(base_path()), + 'chroot' => realpath(base_path()), /** * Whether to use Unicode fonts or not. @@ -82,20 +81,19 @@ return [ * When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a * document must be present in your fonts, however. */ - "DOMPDF_UNICODE_ENABLED" => true, + 'unicode_enabled' => true, /** * Whether to enable font subsetting or not. */ - "DOMPDF_ENABLE_FONTSUBSETTING" => false, + 'enable_fontsubsetting' => false, /** - * The PDF rendering backend to use + * The PDF rendering backend to use. * * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will - * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link - * Canvas_Factory} ultimately determines which rendering class to instantiate + * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link * Canvas_Factory} ultimately determines which rendering class to instantiate * based on this setting. * * Both PDFLib & CPDF rendering backends provide sufficient rendering @@ -117,10 +115,10 @@ return [ * @link http://www.ros.co.nz/pdf * @link http://www.php.net/image */ - "DOMPDF_PDF_BACKEND" => "CPDF", + 'pdf_backend' => 'CPDF', /** - * PDFlib license key + * PDFlib license key. * * If you are using a licensed, commercial version of PDFlib, specify * your license key here. If you are using PDFlib-Lite or are evaluating @@ -143,7 +141,7 @@ return [ * the desired content might be different (e.g. screen or projection view of html file). * Therefore allow specification of content here. */ - "DOMPDF_DEFAULT_MEDIA_TYPE" => "print", + 'default_media_type' => 'print', /** * The default paper size. @@ -152,18 +150,19 @@ return [ * * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.) */ - "DOMPDF_DEFAULT_PAPER_SIZE" => "a4", + 'default_paper_size' => 'a4', /** - * The default font family + * The default font family. * * Used if no suitable fonts can be found. This must exist in the font folder. + * * @var string */ - "DOMPDF_DEFAULT_FONT" => "dejavu sans", + 'default_font' => 'dejavu sans', /** - * Image DPI setting + * Image DPI setting. * * This setting determines the default DPI setting for images and fonts. The * DPI may be overridden for inline images by explictly setting the @@ -195,10 +194,10 @@ return [ * * @var int */ - "DOMPDF_DPI" => 96, + 'dpi' => 96, /** - * Enable inline PHP + * Enable inline PHP. * * If this setting is set to true then DOMPDF will automatically evaluate * inline PHP contained within tags. @@ -209,20 +208,20 @@ return [ * * @var bool */ - "DOMPDF_ENABLE_PHP" => false, + 'enable_php' => false, /** - * Enable inline Javascript + * Enable inline Javascript. * * If this setting is set to true then DOMPDF will automatically insert * JavaScript code contained within tags. * * @var bool */ - "DOMPDF_ENABLE_JAVASCRIPT" => false, + 'enable_javascript' => false, /** - * Enable remote file access + * Enable remote file access. * * If this setting is set to true, DOMPDF will access remote sites for * images and CSS files as required. @@ -238,29 +237,27 @@ return [ * * @var bool */ - "DOMPDF_ENABLE_REMOTE" => true, + 'enable_remote' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false), /** - * A ratio applied to the fonts height to be more like browsers' line height + * A ratio applied to the fonts height to be more like browsers' line height. */ - "DOMPDF_FONT_HEIGHT_RATIO" => 1.1, + 'font_height_ratio' => 1.1, /** - * Enable CSS float + * Enable CSS float. * * Allows people to disabled CSS float support + * * @var bool */ - "DOMPDF_ENABLE_CSS_FLOAT" => true, - + 'enable_css_float' => true, /** - * Use the more-than-experimental HTML5 Lib parser + * Use the more-than-experimental HTML5 Lib parser. */ - "DOMPDF_ENABLE_HTML5PARSER" => true, - + 'enable_html5parser' => true, ], - ]; diff --git a/app/Config/filesystems.php b/app/Config/filesystems.php index 30a5c5369..95fc35c2a 100644 --- a/app/Config/filesystems.php +++ b/app/Config/filesystems.php @@ -34,7 +34,7 @@ return [ 'local' => [ 'driver' => 'local', - 'root' => public_path(), + 'root' => public_path(), ], 'local_secure' => [ @@ -43,12 +43,12 @@ return [ ], 's3' => [ - 'driver' => 's3', - 'key' => env('STORAGE_S3_KEY', 'your-key'), - 'secret' => env('STORAGE_S3_SECRET', 'your-secret'), - 'region' => env('STORAGE_S3_REGION', 'your-region'), - 'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'), - 'endpoint' => env('STORAGE_S3_ENDPOINT', null), + 'driver' => 's3', + 'key' => env('STORAGE_S3_KEY', 'your-key'), + 'secret' => env('STORAGE_S3_SECRET', 'your-secret'), + 'region' => env('STORAGE_S3_REGION', 'your-region'), + 'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'), + 'endpoint' => env('STORAGE_S3_ENDPOINT', null), 'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null, ], diff --git a/app/Config/hashing.php b/app/Config/hashing.php index 756718ce2..585ee094c 100644 --- a/app/Config/hashing.php +++ b/app/Config/hashing.php @@ -29,9 +29,9 @@ return [ // passwords are hashed using the Argon algorithm. These will allow you // to control the amount of time it takes to hash the given password. 'argon' => [ - 'memory' => 1024, + 'memory' => 1024, 'threads' => 2, - 'time' => 2, + 'time' => 2, ], ]; diff --git a/app/Config/logging.php b/app/Config/logging.php index afd56e482..220aa0607 100644 --- a/app/Config/logging.php +++ b/app/Config/logging.php @@ -30,66 +30,66 @@ return [ // "custom", "stack" 'channels' => [ 'stack' => [ - 'driver' => 'stack', - 'channels' => ['daily'], + 'driver' => 'stack', + 'channels' => ['daily'], 'ignore_exceptions' => false, ], 'single' => [ 'driver' => 'single', - 'path' => storage_path('logs/laravel.log'), - 'level' => 'debug', - 'days' => 14, + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + 'days' => 14, ], 'daily' => [ 'driver' => 'daily', - 'path' => storage_path('logs/laravel.log'), - 'level' => 'debug', - 'days' => 7, + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + 'days' => 7, ], 'slack' => [ - 'driver' => 'slack', - 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', - 'emoji' => ':boom:', - 'level' => 'critical', + 'emoji' => ':boom:', + 'level' => 'critical', ], 'stderr' => [ - 'driver' => 'monolog', + 'driver' => 'monolog', 'handler' => StreamHandler::class, - 'with' => [ + 'with' => [ 'stream' => 'php://stderr', ], ], 'syslog' => [ 'driver' => 'syslog', - 'level' => 'debug', + 'level' => 'debug', ], 'errorlog' => [ 'driver' => 'errorlog', - 'level' => 'debug', + 'level' => 'debug', ], // Custom errorlog implementation that logs out a plain, // non-formatted message intended for the webserver log. 'errorlog_plain_webserver' => [ - 'driver' => 'monolog', - 'level' => 'debug', - 'handler' => ErrorLogHandler::class, - 'handler_with' => [4], - 'formatter' => LineFormatter::class, + 'driver' => 'monolog', + 'level' => 'debug', + 'handler' => ErrorLogHandler::class, + 'handler_with' => [4], + 'formatter' => LineFormatter::class, 'formatter_with' => [ - 'format' => "%message%", + 'format' => '%message%', ], ], 'null' => [ - 'driver' => 'monolog', + 'driver' => 'monolog', 'handler' => NullHandler::class, ], @@ -101,7 +101,6 @@ return [ ], ], - // Failed Login Message // Allows a configurable message to be logged when a login request fails. 'failed_login' => [ diff --git a/app/Config/mail.php b/app/Config/mail.php index abdbd382c..34b28fe2a 100644 --- a/app/Config/mail.php +++ b/app/Config/mail.php @@ -23,7 +23,7 @@ return [ // Global "From" address & name 'from' => [ 'address' => env('MAIL_FROM', 'mail@bookstackapp.com'), - 'name' => env('MAIL_FROM_NAME', 'BookStack') + 'name' => env('MAIL_FROM_NAME', 'BookStack'), ], // Email encryption protocol diff --git a/app/Config/queue.php b/app/Config/queue.php index 46f6962c5..0c79fcdd2 100644 --- a/app/Config/queue.php +++ b/app/Config/queue.php @@ -17,24 +17,23 @@ return [ // Queue connection configuration 'connections' => [ - 'sync' => [ 'driver' => 'sync', ], 'database' => [ - 'driver' => 'database', - 'table' => 'jobs', - 'queue' => 'default', + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', 'retry_after' => 90, ], 'redis' => [ - 'driver' => 'redis', - 'connection' => 'default', - 'queue' => env('REDIS_QUEUE', 'default'), + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => 90, - 'block_for' => null, + 'block_for' => null, ], ], diff --git a/app/Config/saml2.php b/app/Config/saml2.php index 8ba969549..fe311057c 100644 --- a/app/Config/saml2.php +++ b/app/Config/saml2.php @@ -31,7 +31,6 @@ return [ // Overrides, in JSON format, to the configuration passed to underlying onelogin library. 'onelogin_overrides' => env('SAML2_ONELOGIN_OVERRIDES', null), - 'onelogin' => [ // If 'strict' is True, then the PHP Toolkit will reject unsigned // or unencrypted messages if it expects them signed or encrypted @@ -81,7 +80,7 @@ return [ 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', // Usually x509cert and privateKey of the SP are provided by files placed at // the certs folder. But we can also provide them with the following parameters - 'x509cert' => '', + 'x509cert' => '', 'privateKey' => '', ], // Identity Provider Data that we want connect with our SP diff --git a/app/Config/services.php b/app/Config/services.php index 0f9f78d1e..2d7253fb8 100644 --- a/app/Config/services.php +++ b/app/Config/services.php @@ -28,16 +28,16 @@ return [ 'redirect' => env('APP_URL') . '/login/service/github/callback', 'name' => 'GitHub', 'auto_register' => env('GITHUB_AUTO_REGISTER', false), - 'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false), ], 'google' => [ - 'client_id' => env('GOOGLE_APP_ID', false), - 'client_secret' => env('GOOGLE_APP_SECRET', false), - 'redirect' => env('APP_URL') . '/login/service/google/callback', - 'name' => 'Google', - 'auto_register' => env('GOOGLE_AUTO_REGISTER', false), - 'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false), + 'client_id' => env('GOOGLE_APP_ID', false), + 'client_secret' => env('GOOGLE_APP_SECRET', false), + 'redirect' => env('APP_URL') . '/login/service/google/callback', + 'name' => 'Google', + 'auto_register' => env('GOOGLE_AUTO_REGISTER', false), + 'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false), 'select_account' => env('GOOGLE_SELECT_ACCOUNT', false), ], @@ -47,7 +47,7 @@ return [ 'redirect' => env('APP_URL') . '/login/service/slack/callback', 'name' => 'Slack', 'auto_register' => env('SLACK_AUTO_REGISTER', false), - 'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false), ], 'facebook' => [ @@ -56,7 +56,7 @@ return [ 'redirect' => env('APP_URL') . '/login/service/facebook/callback', 'name' => 'Facebook', 'auto_register' => env('FACEBOOK_AUTO_REGISTER', false), - 'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false), ], 'twitter' => [ @@ -65,27 +65,27 @@ return [ 'redirect' => env('APP_URL') . '/login/service/twitter/callback', 'name' => 'Twitter', 'auto_register' => env('TWITTER_AUTO_REGISTER', false), - 'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false), ], 'azure' => [ 'client_id' => env('AZURE_APP_ID', false), 'client_secret' => env('AZURE_APP_SECRET', false), - 'tenant' => env('AZURE_TENANT', false), + 'tenant' => env('AZURE_TENANT', false), 'redirect' => env('APP_URL') . '/login/service/azure/callback', 'name' => 'Microsoft Azure', 'auto_register' => env('AZURE_AUTO_REGISTER', false), - 'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false), ], 'okta' => [ - 'client_id' => env('OKTA_APP_ID'), + 'client_id' => env('OKTA_APP_ID'), 'client_secret' => env('OKTA_APP_SECRET'), - 'redirect' => env('APP_URL') . '/login/service/okta/callback', - 'base_url' => env('OKTA_BASE_URL'), + 'redirect' => env('APP_URL') . '/login/service/okta/callback', + 'base_url' => env('OKTA_BASE_URL'), 'name' => 'Okta', 'auto_register' => env('OKTA_AUTO_REGISTER', false), - 'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false), ], 'gitlab' => [ @@ -95,45 +95,45 @@ return [ 'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances 'name' => 'GitLab', 'auto_register' => env('GITLAB_AUTO_REGISTER', false), - 'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false), ], 'twitch' => [ - 'client_id' => env('TWITCH_APP_ID'), + 'client_id' => env('TWITCH_APP_ID'), 'client_secret' => env('TWITCH_APP_SECRET'), - 'redirect' => env('APP_URL') . '/login/service/twitch/callback', + 'redirect' => env('APP_URL') . '/login/service/twitch/callback', 'name' => 'Twitch', 'auto_register' => env('TWITCH_AUTO_REGISTER', false), - 'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false), ], 'discord' => [ - 'client_id' => env('DISCORD_APP_ID'), + 'client_id' => env('DISCORD_APP_ID'), 'client_secret' => env('DISCORD_APP_SECRET'), - 'redirect' => env('APP_URL') . '/login/service/discord/callback', - 'name' => 'Discord', + 'redirect' => env('APP_URL') . '/login/service/discord/callback', + 'name' => 'Discord', 'auto_register' => env('DISCORD_AUTO_REGISTER', false), - 'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false), + 'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false), ], 'ldap' => [ - 'server' => env('LDAP_SERVER', false), - 'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false), - 'dn' => env('LDAP_DN', false), - 'pass' => env('LDAP_PASS', false), - 'base_dn' => env('LDAP_BASE_DN', false), - 'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'), - 'version' => env('LDAP_VERSION', false), - 'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'), - 'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'), + 'server' => env('LDAP_SERVER', false), + 'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false), + 'dn' => env('LDAP_DN', false), + 'pass' => env('LDAP_PASS', false), + 'base_dn' => env('LDAP_BASE_DN', false), + 'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'), + 'version' => env('LDAP_VERSION', false), + 'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'), + 'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'), 'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'), - 'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false), - 'user_to_groups' => env('LDAP_USER_TO_GROUPS', false), - 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), - 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false), - 'tls_insecure' => env('LDAP_TLS_INSECURE', false), - 'start_tls' => env('LDAP_START_TLS', false), - 'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null), + 'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false), + 'user_to_groups' => env('LDAP_USER_TO_GROUPS', false), + 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), + 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false), + 'tls_insecure' => env('LDAP_TLS_INSECURE', false), + 'start_tls' => env('LDAP_START_TLS', false), + 'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null), ], ]; diff --git a/app/Config/session.php b/app/Config/session.php index c750e1ef9..4bbb78901 100644 --- a/app/Config/session.php +++ b/app/Config/session.php @@ -1,6 +1,6 @@ [ - 'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false), + 'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false), 'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'), - 'bookshelf_view_type' =>env('APP_VIEWS_BOOKSHELF', 'grid'), - 'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'), + 'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'), + 'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'), ], ]; diff --git a/app/Config/snappy.php b/app/Config/snappy.php index f347eda23..0f012bef6 100644 --- a/app/Config/snappy.php +++ b/app/Config/snappy.php @@ -14,7 +14,7 @@ return [ 'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false), 'timeout' => false, 'options' => [ - 'outline' => true + 'outline' => true, ], 'env' => [], ], diff --git a/app/Console/Commands/CleanupImages.php b/app/Console/Commands/CleanupImages.php index 93ca367a2..722150197 100644 --- a/app/Console/Commands/CleanupImages.php +++ b/app/Console/Commands/CleanupImages.php @@ -25,11 +25,11 @@ class CleanupImages extends Command */ protected $description = 'Cleanup images and drawings'; - protected $imageService; /** * Create a new command instance. + * * @param \BookStack\Uploads\ImageService $imageService */ public function __construct(ImageService $imageService) @@ -63,6 +63,7 @@ class CleanupImages extends Command $this->comment($deleteCount . ' images found that would have been deleted'); $this->showDeletedImages($deleted); $this->comment('Run with -f or --force to perform deletions'); + return; } diff --git a/app/Console/Commands/ClearViews.php b/app/Console/Commands/ClearViews.php index 693d93639..0fc6c0195 100644 --- a/app/Console/Commands/ClearViews.php +++ b/app/Console/Commands/ClearViews.php @@ -23,7 +23,6 @@ class ClearViews extends Command /** * Create a new command instance. - * */ public function __construct() { diff --git a/app/Console/Commands/CopyShelfPermissions.php b/app/Console/Commands/CopyShelfPermissions.php index d220c59f9..32adf0683 100644 --- a/app/Console/Commands/CopyShelfPermissions.php +++ b/app/Console/Commands/CopyShelfPermissions.php @@ -54,13 +54,14 @@ class CopyShelfPermissions extends Command if (!$cascadeAll && !$shelfSlug) { $this->error('Either a --slug or --all option must be provided.'); + return; } if ($cascadeAll) { $continue = $this->confirm( - 'Permission settings for all shelves will be cascaded. '. - 'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. '. + 'Permission settings for all shelves will be cascaded. ' . + 'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' . 'Are you sure you want to proceed?' ); diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php index 3d1a3dca0..a0fb8f315 100644 --- a/app/Console/Commands/CreateAdmin.php +++ b/app/Console/Commands/CreateAdmin.php @@ -38,8 +38,9 @@ class CreateAdmin extends Command /** * Execute the console command. * - * @return mixed * @throws \BookStack\Exceptions\NotFoundException + * + * @return mixed */ public function handle() { @@ -71,7 +72,6 @@ class CreateAdmin extends Command 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->downloadAndAssignUserAvatar($user); diff --git a/app/Console/Commands/DeleteUsers.php b/app/Console/Commands/DeleteUsers.php index c73c883de..5627dd1f8 100644 --- a/app/Console/Commands/DeleteUsers.php +++ b/app/Console/Commands/DeleteUsers.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class DeleteUsers extends Command { - /** * The name and signature of the console command. * @@ -47,7 +46,7 @@ class DeleteUsers extends Command continue; } $this->userRepo->destroy($user); - ++$numDeleted; + $numDeleted++; } $this->info("Deleted $numDeleted of $totalUsers total users."); } else { diff --git a/app/Console/Commands/ResetMfa.php b/app/Console/Commands/ResetMfa.php new file mode 100644 index 000000000..031bec04b --- /dev/null +++ b/app/Console/Commands/ResetMfa.php @@ -0,0 +1,77 @@ +option('id'); + $email = $this->option('email'); + if (!$id && !$email) { + $this->error('Either a --id= or --email= option must be provided.'); + + return 1; + } + + /** @var User $user */ + $field = $id ? 'id' : 'email'; + $value = $id ?: $email; + $user = User::query() + ->where($field, '=', $value) + ->first(); + + if (!$user) { + $this->error("A user where {$field}={$value} could not be found."); + + return 1; + } + + $this->info("This will delete any configure multi-factor authentication methods for user: \n- ID: {$user->id}\n- Name: {$user->name}\n- Email: {$user->email}\n"); + $this->info('If multi-factor authentication is required for this user they will be asked to reconfigure their methods on next login.'); + $confirm = $this->confirm('Are you sure you want to proceed?'); + if ($confirm) { + $user->mfaValues()->delete(); + $this->info('User MFA methods have been reset.'); + + return 0; + } + + return 1; + } +} diff --git a/app/Console/Commands/UpdateUrl.php b/app/Console/Commands/UpdateUrl.php index 2a1688468..a4bb6cf22 100644 --- a/app/Console/Commands/UpdateUrl.php +++ b/app/Console/Commands/UpdateUrl.php @@ -4,7 +4,6 @@ namespace BookStack\Console\Commands; use Illuminate\Console\Command; use Illuminate\Database\Connection; -use Illuminate\Support\Facades\DB; class UpdateUrl extends Command { @@ -49,7 +48,8 @@ class UpdateUrl extends Command $urlPattern = '/https?:\/\/(.+)/'; if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) { - $this->error("The given urls are expected to be full urls starting with http:// or https://"); + $this->error('The given urls are expected to be full urls starting with http:// or https://'); + return 1; } @@ -58,11 +58,11 @@ class UpdateUrl extends Command } $columnsToUpdateByTable = [ - "attachments" => ["path"], - "pages" => ["html", "text", "markdown"], - "images" => ["url"], - "settings" => ["value"], - "comments" => ["html", "text"], + 'attachments' => ['path'], + 'pages' => ['html', 'text', 'markdown'], + 'images' => ['url'], + 'settings' => ['value'], + 'comments' => ['html', 'text'], ]; foreach ($columnsToUpdateByTable as $table => $columns) { @@ -73,7 +73,7 @@ class UpdateUrl extends Command } $jsonColumnsToUpdateByTable = [ - "settings" => ["value"], + 'settings' => ['value'], ]; foreach ($jsonColumnsToUpdateByTable as $table => $columns) { @@ -85,10 +85,11 @@ class UpdateUrl extends Command } } - $this->info("URL update procedure complete."); + $this->info('URL update procedure complete.'); $this->info('============================================================================'); $this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.'); $this->info('============================================================================'); + return 0; } @@ -100,8 +101,9 @@ class UpdateUrl extends Command { $oldQuoted = $this->db->getPdo()->quote($oldUrl); $newQuoted = $this->db->getPdo()->quote($newUrl); + return $this->db->table($table)->update([ - $column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})") + $column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})"), ]); } @@ -112,8 +114,8 @@ class UpdateUrl extends Command protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool { $dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n"; - $dangerWarning .= "Are you sure you want to proceed?"; - $backupConfirmation = "This operation could cause issues if used incorrectly. Have you made a backup of your existing database?"; + $dangerWarning .= 'Are you sure you want to proceed?'; + $backupConfirmation = 'This operation could cause issues if used incorrectly. Have you made a backup of your existing database?'; return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation); } diff --git a/app/Console/Commands/UpgradeDatabaseEncoding.php b/app/Console/Commands/UpgradeDatabaseEncoding.php index a17fc9523..32808729a 100644 --- a/app/Console/Commands/UpgradeDatabaseEncoding.php +++ b/app/Console/Commands/UpgradeDatabaseEncoding.php @@ -23,7 +23,6 @@ class UpgradeDatabaseEncoding extends Command /** * Create a new command instance. - * */ public function __construct() { @@ -44,12 +43,12 @@ class UpgradeDatabaseEncoding extends Command $database = DB::getDatabaseName(); $tables = DB::select('SHOW TABLES'); - $this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); - $this->line('USE `'.$database.'`;'); + $this->line('ALTER DATABASE `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + $this->line('USE `' . $database . '`;'); $key = 'Tables_in_' . $database; foreach ($tables as $table) { $tableName = $table->$key; - $this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + $this->line('ALTER TABLE `' . $tableName . '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); } DB::setDefaultConnection($connection); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e75d93801..11c8018c8 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -1,4 +1,6 @@ -load(__DIR__.'/Commands'); + $this->load(__DIR__ . '/Commands'); } } diff --git a/app/Entities/BreadcrumbsViewComposer.php b/app/Entities/BreadcrumbsViewComposer.php index cf7cf296c..797162dfb 100644 --- a/app/Entities/BreadcrumbsViewComposer.php +++ b/app/Entities/BreadcrumbsViewComposer.php @@ -1,4 +1,6 @@ -bookshelf = new Bookshelf(); @@ -55,15 +55,16 @@ class EntityProvider /** * Fetch all core entity types as an associated array * with their basic names as the keys. + * * @return array */ public function all(): array { return [ 'bookshelf' => $this->bookshelf, - 'book' => $this->book, - 'chapter' => $this->chapter, - 'page' => $this->page, + 'book' => $this->book, + 'chapter' => $this->chapter, + 'page' => $this->page, ]; } @@ -73,6 +74,7 @@ class EntityProvider public function get(string $type): Entity { $type = strtolower($type); + return $this->all()[$type]; } @@ -86,6 +88,7 @@ class EntityProvider $model = $this->get($type); $morphClasses[] = $model->getMorphClass(); } + return $morphClasses; } } diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 6c5676765..df30c1c71 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -1,4 +1,6 @@ -directPages()->visible()->get(); $chapters = $this->chapters()->visible()->get(); + return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft'); } } diff --git a/app/Entities/Models/BookChild.php b/app/Entities/Models/BookChild.php index c73fa3959..e1ba0b6f7 100644 --- a/app/Entities/Models/BookChild.php +++ b/app/Entities/Models/BookChild.php @@ -1,20 +1,38 @@ -addSelect(['book_slug' => function ($builder) { + $builder->select('slug') + ->from('books') + ->whereColumn('books.id', '=', 'book_id'); + }]); + }); + } /** - * Scope a query to find items where the the child has the given childSlug + * Scope a query to find items where the child has the given childSlug * where its parent has the bookSlug. */ public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug) diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index 8ffd06d2e..f427baf49 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -1,4 +1,6 @@ - $pages + * @property mixed description */ class Chapter extends BookChild { @@ -15,7 +19,9 @@ class Chapter extends BookChild /** * Get the pages that this chapter contains. + * * @param string $dir + * * @return mixed */ public function pages($dir = 'ASC') @@ -30,7 +36,7 @@ class Chapter extends BookChild { $parts = [ 'books', - urlencode($this->getAttribute('bookSlug') ?? $this->book->slug), + urlencode($this->book_slug ?? $this->book->slug), 'chapter', urlencode($this->slug), trim($path, '/'), diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php index 1be0ba4c6..764c4a1e3 100644 --- a/app/Entities/Models/Deletion.php +++ b/app/Entities/Models/Deletion.php @@ -1,15 +1,18 @@ -forceFill([ - 'deleted_by' => user()->id, + 'deleted_by' => user()->id, 'deletable_type' => $entity->getMorphClass(), - 'deletable_id' => $entity->id, + 'deletable_id' => $entity->id, ]); $record->save(); + return $record; } public function logDescriptor(): string { $deletable = $this->deletable()->first(); + return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}"; } + + /** + * Get a URL for this specific deletion. + */ + public function getUrl($path): string + { + return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/')); + } } diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 561876769..a02926c4d 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -1,4 +1,6 @@ -morphMany(Comment::class, 'entity'); + return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query; } @@ -205,7 +209,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable /** * Check if this instance or class is a certain type of entity. - * Examples of $type are 'page', 'book', 'chapter' + * Examples of $type are 'page', 'book', 'chapter'. */ public static function isA(string $type): bool { @@ -218,6 +222,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable public static function getType(): string { $className = array_slice(explode('\\', static::class), -1, 1)[0]; + return strtolower($className); } @@ -229,6 +234,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable if (mb_strlen($this->name) <= $length) { return $this->name; } + return mb_substr($this->name, 0, $length - 3) . '...'; } @@ -248,14 +254,14 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable $text = $this->getText(); if (mb_strlen($text) > $length) { - $text = mb_substr($text, 0, $length-3) . '...'; + $text = mb_substr($text, 0, $length - 3) . '...'; } return trim($text); } /** - * Get the url of this entity + * Get the url of this entity. */ abstract public function getUrl(string $path = '/'): string; @@ -266,12 +272,13 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable */ public function getParent(): ?Entity { - if ($this->isA('page')) { + if ($this instanceof Page) { return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first(); } - if ($this->isA('chapter')) { + if ($this instanceof Chapter) { return $this->book()->withTrashed()->first(); } + return null; } @@ -285,7 +292,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable } /** - * Index the current entity for search + * Index the current entity for search. */ public function indexForSearch() { @@ -298,6 +305,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable public function refreshSlug(): string { $this->slug = app(SlugGenerator::class)->generate($this); + return $this->slug; } diff --git a/app/Entities/Models/HasCoverImage.php b/app/Entities/Models/HasCoverImage.php index f3a486d18..f665efce6 100644 --- a/app/Entities/Models/HasCoverImage.php +++ b/app/Entities/Models/HasCoverImage.php @@ -1,13 +1,11 @@ 'boolean', + 'draft' => 'boolean', 'template' => 'boolean', ]; @@ -41,22 +45,13 @@ class Page extends BookChild public function scopeVisible(Builder $query): Builder { $query = Permissions::enforceDraftVisibilityOnQuery($query); + return parent::scopeVisible($query); } - /** - * Converts this page into a simplified array. - * @return mixed - */ - public function toSimpleArray() - { - $array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes)); - $array['url'] = $this->getUrl(); - return $array; - } - /** * Get the chapter that this page is in, If applicable. + * * @return BelongsTo */ public function chapter() @@ -66,6 +61,7 @@ class Page extends BookChild /** * Check if this page has a chapter. + * * @return bool */ public function hasChapter() @@ -96,6 +92,7 @@ class Page extends BookChild /** * Get the attachments assigned to this page. + * * @return HasMany */ public function attachments() @@ -110,7 +107,7 @@ class Page extends BookChild { $parts = [ 'books', - urlencode($this->getAttribute('bookSlug') ?? $this->book->slug), + urlencode($this->book_slug ?? $this->book->slug), $this->draft ? 'draft' : 'page', $this->draft ? $this->id : urlencode($this->slug), trim($path, '/'), @@ -120,7 +117,8 @@ class Page extends BookChild } /** - * Get the current revision for the page if existing + * Get the current revision for the page if existing. + * * @return PageRevision|null */ public function getCurrentRevision() @@ -136,6 +134,7 @@ class Page extends BookChild $refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']); $refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown'])); $refreshed->html = (new PageContent($refreshed))->render(); + return $refreshed; } } diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php index 76a3b15ff..c1a74f66b 100644 --- a/app/Entities/Models/PageRevision.php +++ b/app/Entities/Models/PageRevision.php @@ -1,29 +1,32 @@ -make(EntityProvider::class); } -} \ No newline at end of file +} diff --git a/app/Entities/Queries/Popular.php b/app/Entities/Queries/Popular.php index 98db2fe62..e6b22a1c9 100644 --- a/app/Entities/Queries/Popular.php +++ b/app/Entities/Queries/Popular.php @@ -1,5 +1,6 @@ -pluck('viewable') ->filter(); } - -} \ No newline at end of file +} diff --git a/app/Entities/Queries/RecentlyViewed.php b/app/Entities/Queries/RecentlyViewed.php index d528fea44..5a29ecd72 100644 --- a/app/Entities/Queries/RecentlyViewed.php +++ b/app/Entities/Queries/RecentlyViewed.php @@ -1,4 +1,6 @@ -tagRepo = $tagRepo; @@ -27,7 +21,7 @@ class BaseRepo } /** - * Create a new entity in the system + * Create a new entity in the system. */ public function create(Entity $entity, array $input) { @@ -35,7 +29,7 @@ class BaseRepo $entity->forceFill([ 'created_by' => user()->id, 'updated_by' => user()->id, - 'owned_by' => user()->id, + 'owned_by' => user()->id, ]); $entity->refreshSlug(); $entity->save(); @@ -72,6 +66,7 @@ class BaseRepo /** * Update the given items' cover image, or clear it. + * * @throws ImageUploadException * @throws \Exception */ diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php index 27d0b4075..a692bbaf7 100644 --- a/app/Entities/Repos/BookRepo.php +++ b/app/Entities/Repos/BookRepo.php @@ -1,4 +1,6 @@ -baseRepo->create($book, $input); Activity::addForEntity($book, ActivityType::BOOK_CREATE); + return $book; } @@ -101,11 +103,13 @@ class BookRepo { $this->baseRepo->update($book, $input); Activity::addForEntity($book, ActivityType::BOOK_UPDATE); + return $book; } /** * Update the given book's cover image, or clear it. + * * @throws ImageUploadException * @throws Exception */ @@ -116,6 +120,7 @@ class BookRepo /** * Remove a book from the system. + * * @throws Exception */ public function destroy(Book $book) diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index 649f4b0c4..3990bfbdc 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -1,4 +1,6 @@ -baseRepo->create($shelf, $input); $this->updateBooks($shelf, $bookIds); Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE); + return $shelf; } @@ -104,6 +107,7 @@ class BookshelfRepo } Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE); + return $shelf; } @@ -129,6 +133,7 @@ class BookshelfRepo /** * Update the given shelf cover image, or clear it. + * * @throws ImageUploadException * @throws Exception */ @@ -164,6 +169,7 @@ class BookshelfRepo /** * Remove a bookshelf from the system. + * * @throws Exception */ public function destroy(Bookshelf $shelf) diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php index d56874e0d..68330dd57 100644 --- a/app/Entities/Repos/ChapterRepo.php +++ b/app/Entities/Repos/ChapterRepo.php @@ -1,4 +1,6 @@ -priority = (new BookContents($parentBook))->getLastPriority() + 1; $this->baseRepo->create($chapter, $input); Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE); + return $chapter; } @@ -59,11 +61,13 @@ class ChapterRepo { $this->baseRepo->update($chapter, $input); Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE); + return $chapter; } /** * Remove a chapter from the system. + * * @throws Exception */ public function destroy(Chapter $chapter) @@ -77,7 +81,8 @@ class ChapterRepo /** * Move the given chapter into a new parent book. * The $parentIdentifier must be a string of the following format: - * 'book:' (book:5) + * 'book:' (book:5). + * * @throws MoveOperationException */ public function move(Chapter $chapter, string $parentIdentifier): Book diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index 5eb882a02..ffa06d459 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -1,14 +1,16 @@ -orderBy('created_at', 'desc') ->with('page') ->first(); + return $revision ? $revision->page : null; } @@ -119,6 +122,7 @@ class PageRepo public function getUserDraft(Page $page): ?PageRevision { $revision = $this->getUserDraftQuery($page)->first(); + return $revision; } @@ -128,11 +132,11 @@ class PageRepo public function getNewDraftPage(Entity $parent) { $page = (new Page())->forceFill([ - 'name' => trans('entities.pages_initial_name'), + 'name' => trans('entities.pages_initial_name'), 'created_by' => user()->id, - 'owned_by' => user()->id, + 'owned_by' => user()->id, 'updated_by' => user()->id, - 'draft' => true, + 'draft' => true, ]); if ($parent instanceof Chapter) { @@ -144,6 +148,7 @@ class PageRepo $page->save(); $page->refresh()->rebuildPermissions(); + return $page; } @@ -166,6 +171,7 @@ class PageRepo $draft->refresh(); Activity::addForEntity($draft, ActivityType::PAGE_CREATE); + return $draft; } @@ -190,7 +196,7 @@ class PageRepo $this->getUserDraftQuery($page)->delete(); // Save a revision after updating - $summary = trim($input['summary'] ?? ""); + $summary = trim($input['summary'] ?? ''); $htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml; $nameChanged = isset($input['name']) && $input['name'] !== $oldName; $markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown; @@ -199,6 +205,7 @@ class PageRepo } Activity::addForEntity($page, ActivityType::PAGE_UPDATE); + return $page; } @@ -211,8 +218,8 @@ class PageRepo $pageContent = new PageContent($page); if (!empty($input['markdown'] ?? '')) { $pageContent->setNewMarkdown($input['markdown']); - } else { - $pageContent->setNewHTML($input['html'] ?? ''); + } elseif (isset($input['html'])) { + $pageContent->setNewHTML($input['html']); } } @@ -234,6 +241,7 @@ class PageRepo $revision->save(); $this->deleteOldRevisions($page); + return $revision; } @@ -249,6 +257,7 @@ class PageRepo } $page->fill($input); $page->save(); + return $page; } @@ -260,11 +269,13 @@ class PageRepo } $draft->save(); + return $draft; } /** * Destroy a page from the system. + * * @throws Exception */ public function destroy(Page $page) @@ -291,7 +302,7 @@ class PageRepo } else { $content->setNewHTML($revision->html); } - + $page->updated_by = user()->id; $page->refreshSlug(); $page->save(); @@ -301,13 +312,15 @@ class PageRepo $this->savePageRevision($page, $summary); Activity::addForEntity($page, ActivityType::PAGE_RESTORE); + return $page; } /** * Move the given page into a new parent book or chapter. * The $parentIdentifier must be a string of the following format: - * 'book:' (book:5) + * 'book:' (book:5). + * * @throws MoveOperationException * @throws PermissionsException */ @@ -327,12 +340,14 @@ class PageRepo $page->rebuildPermissions(); Activity::addForEntity($page, ActivityType::PAGE_MOVE); + return $parent; } /** * Copy an existing page in the system. * Optionally providing a new parent via string identifier and a new name. + * * @throws MoveOperationException * @throws PermissionsException */ @@ -369,7 +384,8 @@ class PageRepo /** * Find a page parent entity via a identifier string in the format: * {type}:{id} - * Example: (book:5) + * Example: (book:5). + * * @throws MoveOperationException */ protected function findParentByIdentifier(string $identifier): ?Entity @@ -383,6 +399,7 @@ class PageRepo } $parentClass = $entityType === 'book' ? Book::class : Chapter::class; + return $parentClass::visible()->where('id', '=', $entityId)->first(); } @@ -420,6 +437,7 @@ class PageRepo $draft->book_slug = $page->book->slug; $draft->created_by = user()->id; $draft->type = 'update_draft'; + return $draft; } @@ -445,13 +463,14 @@ class PageRepo } /** - * Get a new priority for a page + * Get a new priority for a page. */ protected function getNewPriority(Page $page): int { $parent = $page->getParent(); if ($parent instanceof Chapter) { $lastPage = $parent->pages('desc')->first(); + return $lastPage ? $lastPage->priority + 1 : 0; } diff --git a/app/Entities/Tools/BookContents.php b/app/Entities/Tools/BookContents.php index 71c8f8393..8622d5e12 100644 --- a/app/Entities/Tools/BookContents.php +++ b/app/Entities/Tools/BookContents.php @@ -1,4 +1,6 @@ -where('chapter_id', '=', 0)->max('priority'); $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id) ->max('priority'); + return max($maxChapter, $maxPage, 1); } @@ -43,7 +45,7 @@ class BookContents */ public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection { - $pages = $this->getPages($showDrafts); + $pages = $this->getPages($showDrafts, $renderPages); $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get(); $all = collect()->concat($pages)->concat($chapters); $chapterMap = $chapters->keyBy('id'); @@ -83,6 +85,7 @@ class BookContents if (isset($entity['draft']) && $entity['draft']) { return -100; } + return $entity['priority'] ?? 0; }; } @@ -90,9 +93,11 @@ class BookContents /** * Get the visible pages within this book. */ - protected function getPages(bool $showDrafts = false): Collection + protected function getPages(bool $showDrafts = false, bool $getPageContent = false): Collection { - $query = Page::visible()->where('book_id', '=', $this->book->id); + $query = Page::visible() + ->select($getPageContent ? Page::$contentAttributes : Page::$listAttributes) + ->where('book_id', '=', $this->book->id); if (!$showDrafts) { $query->where('draft', '=', false); @@ -110,9 +115,10 @@ class BookContents * +"parentChapter": false (ID of parent chapter, as string, or false) * +"type": "page" (Entity type of item) * +"book": "1" (Id of book to place item in) - * } + * }. * * Returns a list of books that were involved in the operation. + * * @throws SortOperationException */ public function sortUsingMap(Collection $sortMap): Collection @@ -190,6 +196,7 @@ class BookContents /** * Get the books involved in a sort. * The given sort map should have its models loaded first. + * * @throws SortOperationException */ protected function getBooksInvolvedInSort(Collection $sortMap): Collection @@ -202,7 +209,7 @@ class BookContents $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get(); if (count($books) !== count($bookIdsInvolved)) { - throw new SortOperationException("Could not find all books requested in sort operation"); + throw new SortOperationException('Could not find all books requested in sort operation'); } return $books; diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php index eb8f6862f..05d0ff134 100644 --- a/app/Entities/Tools/ExportFormatter.php +++ b/app/Entities/Tools/ExportFormatter.php @@ -1,8 +1,11 @@ -html = (new PageContent($page))->render(); $pageHtml = view('pages.export', [ - 'page' => $page, + 'page' => $page, 'format' => 'html', ])->render(); + return $this->containHtml($pageHtml); } /** * Convert a chapter to a self-contained HTML file. + * * @throws Throwable */ public function chapterToContainedHtml(Chapter $chapter) @@ -49,43 +54,49 @@ class ExportFormatter }); $html = view('chapters.export', [ 'chapter' => $chapter, - 'pages' => $pages, - 'format' => 'html', + 'pages' => $pages, + 'format' => 'html', ])->render(); + return $this->containHtml($html); } /** * Convert a book to a self-contained HTML file. + * * @throws Throwable */ public function bookToContainedHtml(Book $book) { $bookTree = (new BookContents($book))->getTree(false, true); $html = view('books.export', [ - 'book' => $book, + 'book' => $book, 'bookChildren' => $bookTree, - 'format' => 'html', + 'format' => 'html', ])->render(); + return $this->containHtml($html); } /** * Convert a page to a PDF file. + * * @throws Throwable */ public function pageToPdf(Page $page) { $page->html = (new PageContent($page))->render(); $html = view('pages.export', [ - 'page' => $page, + 'page' => $page, 'format' => 'pdf', ])->render(); + return $this->htmlToPdf($html); } /** * Convert a chapter to a PDF file. + * * @throws Throwable */ public function chapterToPdf(Chapter $chapter) @@ -97,8 +108,8 @@ class ExportFormatter $html = view('chapters.export', [ 'chapter' => $chapter, - 'pages' => $pages, - 'format' => 'pdf', + 'pages' => $pages, + 'format' => 'pdf', ])->render(); return $this->htmlToPdf($html); @@ -106,38 +117,43 @@ class ExportFormatter /** * Convert a book to a PDF file. + * * @throws Throwable */ public function bookToPdf(Book $book) { $bookTree = (new BookContents($book))->getTree(false, true); $html = view('books.export', [ - 'book' => $book, + 'book' => $book, 'bookChildren' => $bookTree, - 'format' => 'pdf', + 'format' => 'pdf', ])->render(); + return $this->htmlToPdf($html); } /** * Convert normal web-page HTML to a PDF. + * * @throws Exception */ protected function htmlToPdf(string $html): string { $containedHtml = $this->containHtml($html); - $useWKHTML = config('snappy.pdf.binary') !== false; + $useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true; if ($useWKHTML) { $pdf = SnappyPDF::loadHTML($containedHtml); $pdf->setOption('print-media-type', true); } else { $pdf = DomPDF::loadHTML($containedHtml); } + return $pdf->output(); } /** * Bundle of the contents of a html file to be self-contained. + * * @throws Exception */ protected function containHtml(string $htmlContent): string @@ -194,6 +210,7 @@ class ExportFormatter $text = html_entity_decode($text); // Add title $text = $page->name . "\n\n" . $text; + return $text; } @@ -207,6 +224,7 @@ class ExportFormatter foreach ($chapter->getVisiblePages() as $page) { $text .= $this->pageToPlainText($page); } + return $text; } @@ -224,6 +242,51 @@ class ExportFormatter $text .= $this->pageToPlainText($bookChild); } } + + return $text; + } + + /** + * Convert a page to a Markdown file. + */ + public function pageToMarkdown(Page $page): string + { + if ($page->markdown) { + return '# ' . $page->name . "\n\n" . $page->markdown; + } + + return '# ' . $page->name . "\n\n" . (new HtmlToMarkdown($page->html))->convert(); + } + + /** + * Convert a chapter to a Markdown file. + */ + public function chapterToMarkdown(Chapter $chapter): string + { + $text = '# ' . $chapter->name . "\n\n"; + $text .= $chapter->description . "\n\n"; + foreach ($chapter->pages as $page) { + $text .= $this->pageToMarkdown($page) . "\n\n"; + } + + return $text; + } + + /** + * Convert a book into a plain text string. + */ + public function bookToMarkdown(Book $book): string + { + $bookTree = (new BookContents($book))->getTree(false, true); + $text = '# ' . $book->name . "\n\n"; + foreach ($bookTree as $bookChild) { + if ($bookChild instanceof Chapter) { + $text .= $this->chapterToMarkdown($bookChild); + } else { + $text .= $this->pageToMarkdown($bookChild); + } + } + return $text; } } diff --git a/app/Entities/Tools/Markdown/CustomListItemRenderer.php b/app/Entities/Tools/Markdown/CustomListItemRenderer.php new file mode 100644 index 000000000..be4cac4a7 --- /dev/null +++ b/app/Entities/Tools/Markdown/CustomListItemRenderer.php @@ -0,0 +1,43 @@ +baseRenderer = new ListItemRenderer(); + } + + /** + * @return HtmlElement|string|null + */ + public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false) + { + $listItem = $this->baseRenderer->render($block, $htmlRenderer, $inTightList); + + if ($this->startsTaskListItem($block)) { + $listItem->setAttribute('class', 'task-list-item'); + } + + return $listItem; + } + + private function startsTaskListItem(ListItem $block): bool + { + $firstChild = $block->firstChild(); + + return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker; + } +} diff --git a/app/Entities/Tools/Markdown/CustomParagraphConverter.php b/app/Entities/Tools/Markdown/CustomParagraphConverter.php new file mode 100644 index 000000000..bd493aa03 --- /dev/null +++ b/app/Entities/Tools/Markdown/CustomParagraphConverter.php @@ -0,0 +1,19 @@ +getAttribute('class'); + if (strpos($class, 'callout') !== false) { + return "<{$element->getTagName()} class=\"{$class}\">{$element->getValue()}getTagName()}>\n\n"; + } + + return parent::convert($element); + } +} diff --git a/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php b/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php index d4984ef08..a8ccfc4f9 100644 --- a/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php +++ b/app/Entities/Tools/Markdown/CustomStrikeThroughExtension.php @@ -1,4 +1,6 @@ -addDelimiterProcessor(new StrikethroughDelimiterProcessor()); diff --git a/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php b/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php index 7de95c263..ca9f434af 100644 --- a/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php +++ b/app/Entities/Tools/Markdown/CustomStrikethroughRenderer.php @@ -1,4 +1,6 @@ -html = $html; + } + + /** + * Run the conversion. + */ + public function convert(): string + { + $converter = new HtmlConverter($this->getConverterEnvironment()); + $html = $this->prepareHtml($this->html); + + return $converter->convert($html); + } + + /** + * Run any pre-processing to the HTML to clean it up manually before conversion. + */ + protected function prepareHtml(string $html): string + { + // Carriage returns can cause whitespace issues in output + $html = str_replace("\r\n", "\n", $html); + // Attributes on the pre tag can cause issues with conversion + return preg_replace('/
/', '
', $html);
+    }
+
+    /**
+     * Get the HTML to Markdown customized environment.
+     * Extends the default provided environment with some BookStack specific tweaks.
+     */
+    protected function getConverterEnvironment(): Environment
+    {
+        $environment = new Environment([
+            'header_style'            => 'atx', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2
+            'suppress_errors'         => true, // Set to false to show warnings when loading malformed HTML
+            'strip_tags'              => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.
+            'strip_placeholder_links' => false, // Set to true to remove  that doesn't have href.
+            'bold_style'              => '**', // DEPRECATED: Set to '__' if you prefer the underlined style
+            'italic_style'            => '*', // DEPRECATED: Set to '_' if you prefer the underlined style
+            'remove_nodes'            => '', // space-separated list of dom nodes that should be removed. example: 'meta style script'
+            'hard_break'              => false, // Set to true to turn 
into `\n` instead of ` \n` + 'list_item_style' => '-', // Set the default character for each
  • in a
      . Can be '-', '*', or '+' + 'preserve_comments' => false, // Set to true to preserve comments, or set to an array of strings to preserve specific comments + 'use_autolinks' => false, // Set to true to use simple link syntax if possible. Will always use []() if set to false + 'table_pipe_escape' => '\|', // Replacement string for pipe characters inside markdown table cells + 'table_caption_side' => 'top', // Set to 'top' or 'bottom' to show content before or after table, null to suppress + ]); + + $environment->addConverter(new BlockquoteConverter()); + $environment->addConverter(new CodeConverter()); + $environment->addConverter(new CommentConverter()); + $environment->addConverter(new DivConverter()); + $environment->addConverter(new EmphasisConverter()); + $environment->addConverter(new HardBreakConverter()); + $environment->addConverter(new HeaderConverter()); + $environment->addConverter(new HorizontalRuleConverter()); + $environment->addConverter(new ImageConverter()); + $environment->addConverter(new LinkConverter()); + $environment->addConverter(new ListBlockConverter()); + $environment->addConverter(new ListItemConverter()); + $environment->addConverter(new CustomParagraphConverter()); + $environment->addConverter(new PreformattedConverter()); + $environment->addConverter(new TextConverter()); + + return $environment; + } +} diff --git a/app/Entities/Tools/NextPreviousContentLocator.php b/app/Entities/Tools/NextPreviousContentLocator.php index bfb0f4a9d..f70abd9b6 100644 --- a/app/Entities/Tools/NextPreviousContentLocator.php +++ b/app/Entities/Tools/NextPreviousContentLocator.php @@ -1,4 +1,6 @@ -relativeBookItem) && $entity->id === $this->relativeBookItem->id; }); + return $index === false ? null : $index; } @@ -64,6 +67,7 @@ class NextPreviousContentLocator $childPages = $item->visible_pages ?? []; $flatOrdered = $flatOrdered->concat($childPages); } + return $flatOrdered; } } diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 381ef172b..b4cc1b81c 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -1,16 +1,20 @@ -addExtension(new CustomStrikeThroughExtension()); $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment; $converter = new CommonMarkConverter([], $environment); + + $environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10); + return $converter->convertToHtml($markdown); } /** - * Convert all base64 image data to saved images + * Convert all base64 image data to saved images. */ public function extractBase64Images(Page $page, string $htmlText): string { @@ -97,6 +103,7 @@ class PageContent // Save image from data with a random name $imageName = 'embedded-image-' . Str::random(8) . '.' . $extension; + try { $image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id); $imageNode->setAttribute('src', $image->url); @@ -171,7 +178,7 @@ class PageContent /** * Set a unique id on the given DOMElement. * A map for existing ID's should be passed in to check for current existence. - * Returns a pair of strings in the format [old_id, new_id] + * Returns a pair of strings in the format [old_id, new_id]. */ protected function setUniqueId(\DOMNode $element, array &$idMap): array { @@ -183,6 +190,7 @@ class PageContent $existingId = $element->getAttribute('id'); if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) { $idMap[$existingId] = true; + return [$existingId, $existingId]; } @@ -200,6 +208,7 @@ class PageContent $element->setAttribute('id', $newId); $idMap[$newId] = true; + return [$existingId, $newId]; } @@ -209,15 +218,16 @@ class PageContent protected function toPlainText(): string { $html = $this->render(true); + return html_entity_decode(strip_tags($html)); } /** - * Render the page for viewing + * Render the page for viewing. */ public function render(bool $blankIncludes = false): string { - $content = $this->page->html; + $content = $this->page->html ?? ''; if (!config('app.allow_content_scripts')) { $content = HtmlContentFilter::removeScripts($content); @@ -233,7 +243,7 @@ class PageContent } /** - * Parse the headers on the page to get a navigation menu + * Parse the headers on the page to get a navigation menu. */ public function getNavigation(string $htmlContent): array { @@ -243,7 +253,7 @@ class PageContent $doc = $this->loadDocumentFromHtml($htmlContent); $xPath = new DOMXPath($doc); - $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); + $headers = $xPath->query('//h1|//h2|//h3|//h4|//h5|//h6'); return $headers ? $this->headerNodesToLevelList($headers) : []; } @@ -260,9 +270,9 @@ class PageContent return [ 'nodeName' => strtolower($header->nodeName), - 'level' => intval(str_replace('h', '', $header->nodeName)), - 'link' => '#' . $header->getAttribute('id'), - 'text' => $text, + 'level' => intval(str_replace('h', '', $header->nodeName)), + 'link' => '#' . $header->getAttribute('id'), + 'text' => $text, ]; })->filter(function ($header) { return mb_strlen($header['text']) > 0; @@ -272,6 +282,7 @@ class PageContent $levelChange = ($tree->pluck('level')->min() - 1); $tree = $tree->map(function ($header) use ($levelChange) { $header['level'] -= ($levelChange); + return $header; }); @@ -325,7 +336,6 @@ class PageContent return $html; } - /** * Fetch the content from a specific section of the given page. */ @@ -365,6 +375,7 @@ class PageContent $doc = new DOMDocument(); $html = '' . $html . ''; $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + return $doc; } } diff --git a/app/Entities/Tools/PageEditActivity.php b/app/Entities/Tools/PageEditActivity.php index 79de5c827..a88dea830 100644 --- a/app/Entities/Tools/PageEditActivity.php +++ b/app/Entities/Tools/PageEditActivity.php @@ -1,4 +1,6 @@ -activePageEditingQuery(60)->get(); $count = $pageDraftEdits->count(); - $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]); + $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]) : trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]); $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]); + return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]); } /** * Get the message to show when the user will be editing one of their drafts. + * * @param PageRevision $draft + * * @return string */ public function getEditingActiveDraftMessage(PageRevision $draft): string @@ -51,6 +56,7 @@ class PageEditActivity if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) { return $message; } + return $message . "\n" . trans('entities.pages_draft_edited_notification'); } diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index 8a27ce75b..4e8351776 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -1,4 +1,6 @@ -keys()->map(function ($action) use ($roleId) { return [ 'role_id' => $roleId, - 'action' => strtolower($action), - ] ; + 'action' => strtolower($action), + ]; }); }); } diff --git a/app/Entities/Tools/SearchIndex.php b/app/Entities/Tools/SearchIndex.php index 81a5022ce..cc0b32d6a 100644 --- a/app/Entities/Tools/SearchIndex.php +++ b/app/Entities/Tools/SearchIndex.php @@ -1,4 +1,6 @@ -searchTerm = $searchTerm; $this->entityProvider = $entityProvider; } - /** * Index the given entity. */ @@ -42,7 +42,8 @@ class SearchIndex } /** - * Index multiple Entities at once + * Index multiple Entities at once. + * * @param Entity[] $entities */ protected function indexEntities(array $entities) @@ -110,8 +111,8 @@ class SearchIndex $terms = []; foreach ($tokenMap as $token => $count) { $terms[] = [ - 'term' => $token, - 'score' => $count * $scoreAdjustment + 'term' => $token, + 'score' => $count * $scoreAdjustment, ]; } diff --git a/app/Entities/Tools/SearchOptions.php b/app/Entities/Tools/SearchOptions.php index 6c03c57a6..7913e0969 100644 --- a/app/Entities/Tools/SearchOptions.php +++ b/app/Entities/Tools/SearchOptions.php @@ -1,10 +1,11 @@ - $value) { $instance->$type = $value; } + return $instance; } @@ -67,6 +69,7 @@ class SearchOptions if (isset($inputs['types']) && count($inputs['types']) < 4) { $instance->filters['type'] = implode('|', $inputs['types']); } + return $instance; } @@ -77,15 +80,15 @@ class SearchOptions { $terms = [ 'searches' => [], - 'exacts' => [], - 'tags' => [], - 'filters' => [] + 'exacts' => [], + 'tags' => [], + 'filters' => [], ]; $patterns = [ - 'exacts' => '/"(.*?)"/', - 'tags' => '/\[(.*?)\]/', - 'filters' => '/\{(.*?)\}/' + 'exacts' => '/"(.*?)"/', + 'tags' => '/\[(.*?)\]/', + 'filters' => '/\{(.*?)\}/', ]; // Parse special terms diff --git a/app/Entities/Tools/SearchRunner.php b/app/Entities/Tools/SearchRunner.php index fc127f906..8e18408bd 100644 --- a/app/Entities/Tools/SearchRunner.php +++ b/app/Entities/Tools/SearchRunner.php @@ -1,4 +1,6 @@ -=', '=', '<', '>', 'like', '!=']; - public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService) { $this->entityProvider = $entityProvider; @@ -56,7 +56,7 @@ class SearchRunner if ($entityType !== 'all') { $entityTypesToSearch = $entityType; - } else if (isset($searchOpts->filters['type'])) { + } elseif (isset($searchOpts->filters['type'])) { $entityTypesToSearch = explode('|', $searchOpts->filters['type']); } @@ -78,16 +78,15 @@ class SearchRunner } return [ - 'total' => $total, - 'count' => count($results), + 'total' => $total, + 'count' => count($results), 'has_more' => $hasMore, - 'results' => $results->sortByDesc('score')->values(), + 'results' => $results->sortByDesc('score')->values(), ]; } - /** - * Search a book for entities + * Search a book for entities. */ public function searchBook(int $bookId, string $searchString): Collection { @@ -108,12 +107,13 @@ class SearchRunner } /** - * Search a chapter for entities + * Search a chapter for entities. */ public function searchChapter(int $chapterId, string $searchString): Collection { $opts = SearchOptions::fromString($searchString); $pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get(); + return $pages->sortByDesc('score'); } @@ -121,6 +121,7 @@ class SearchRunner * Search across a particular entity type. * Setting getCount = true will return the total * matching instead of the items themselves. + * * @return \Illuminate\Database\Eloquent\Collection|int|static[] */ protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false) @@ -130,12 +131,13 @@ class SearchRunner return $query->count(); } - $query = $query->skip(($page-1) * $count)->take($count); + $query = $query->skip(($page - 1) * $count)->take($count); + return $query->get(); } /** - * Create a search query for an entity + * Create a search query for an entity. */ protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder { @@ -149,20 +151,20 @@ class SearchRunner $subQuery->where('entity_type', '=', $entity->getMorphClass()); $subQuery->where(function (Builder $query) use ($searchOpts) { foreach ($searchOpts->searches as $inputTerm) { - $query->orWhere('term', 'like', $inputTerm .'%'); + $query->orWhere('term', 'like', $inputTerm . '%'); } })->groupBy('entity_type', 'entity_id'); $entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) { $join->on('id', '=', 'entity_id'); - })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc'); + })->selectRaw($entity->getTable() . '.*, s.score')->orderBy('score', 'desc'); $entitySelect->mergeBindings($subQuery); } // Handle exact term matching foreach ($searchOpts->exacts as $inputTerm) { $entitySelect->where(function (EloquentBuilder $query) use ($inputTerm, $entity) { - $query->where('name', 'like', '%'.$inputTerm .'%') - ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); + $query->where('name', 'like', '%' . $inputTerm . '%') + ->orWhere($entity->textField, 'like', '%' . $inputTerm . '%'); }); } @@ -191,6 +193,7 @@ class SearchRunner foreach ($this->queryOperators as $operator) { $escapedOperators[] = preg_quote($operator); } + return join('|', $escapedOperators); } @@ -199,7 +202,7 @@ class SearchRunner */ protected function applyTagSearch(EloquentBuilder $query, string $tagTerm): EloquentBuilder { - preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); + preg_match('/^(.*?)((' . $this->getRegexEscapedOperators() . ')(.*?))?$/', $tagTerm, $tagSplit); $query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) { $tagName = $tagSplit[1]; $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; @@ -222,13 +225,13 @@ class SearchRunner $query->where('name', '=', $tagName); } }); + return $query; } /** - * Custom entity search filters + * Custom entity search filters. */ - protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input) { try { @@ -298,7 +301,7 @@ class SearchRunner protected function filterInName(EloquentBuilder $query, Entity $model, $input) { - $query->where('name', 'like', '%' .$input. '%'); + $query->where('name', 'like', '%' . $input . '%'); } protected function filterInTitle(EloquentBuilder $query, Entity $model, $input) @@ -308,7 +311,7 @@ class SearchRunner protected function filterInBody(EloquentBuilder $query, Entity $model, $input) { - $query->where($model->textField, 'like', '%' .$input. '%'); + $query->where($model->textField, 'like', '%' . $input . '%'); } protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input) @@ -338,16 +341,14 @@ class SearchRunner } } - /** - * Sorting filter options + * Sorting filter options. */ - protected function sortByLastCommented(EloquentBuilder $query, Entity $model) { $commentsTable = $this->db->getTablePrefix() . 'comments'; $morphClass = str_replace('\\', '\\\\', $model->getMorphClass()); - $commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments'); + $commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM ' . $commentsTable . ' c1 LEFT JOIN ' . $commentsTable . ' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \'' . $morphClass . '\' AND c2.created_at IS NULL) as comments'); $query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc'); } diff --git a/app/Entities/Tools/ShelfContext.php b/app/Entities/Tools/ShelfContext.php index f3849bbb4..50d798171 100644 --- a/app/Entities/Tools/ShelfContext.php +++ b/app/Entities/Tools/ShelfContext.php @@ -1,4 +1,6 @@ -get($entityType)->visible()->findOrFail($entityId); + $entity = (new EntityProvider())->get($entityType)->visible()->findOrFail($entityId); $entities = []; // Page in chapter @@ -29,7 +30,7 @@ class SiblingFetcher // Book // Gets just the books in a shelf if shelf is in context if ($entity->isA('book')) { - $contextShelf = (new ShelfContext)->getContextualShelfForBook($entity); + $contextShelf = (new ShelfContext())->getContextualShelfForBook($entity); if ($contextShelf) { $entities = $contextShelf->visibleBooks()->get(); } else { diff --git a/app/Entities/Tools/SlugGenerator.php b/app/Entities/Tools/SlugGenerator.php index 4501279f2..52e5700da 100644 --- a/app/Entities/Tools/SlugGenerator.php +++ b/app/Entities/Tools/SlugGenerator.php @@ -1,4 +1,6 @@ -slugInUse($slug, $model)) { $slug .= '-' . Str::random(3); } + return $slug; } @@ -26,9 +28,10 @@ class SlugGenerator protected function formatNameAsSlug(string $name): string { $slug = Str::slug($name); - if ($slug === "") { + if ($slug === '') { $slug = substr(md5(rand(1, 500)), 0, 5); } + return $slug; } diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 0b6081ae4..82569278e 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -1,11 +1,13 @@ -destroyCommonRelations($shelf); $shelf->forceDelete(); + return 1; } /** * Remove a book from the system. * Destroys any child chapters and pages. + * * @throws Exception */ protected function destroyBook(Book $book): int @@ -120,12 +127,14 @@ class TrashCan $this->destroyCommonRelations($book); $book->forceDelete(); + return $count + 1; } /** * Remove a chapter from the system. * Destroys all pages within. + * * @throws Exception */ protected function destroyChapter(Chapter $chapter): int @@ -141,11 +150,13 @@ class TrashCan $this->destroyCommonRelations($chapter); $chapter->forceDelete(); + return $count + 1; } /** * Remove a page from the system. + * * @throws Exception */ protected function destroyPage(Page $page): int @@ -160,6 +171,7 @@ class TrashCan } $page->forceDelete(); + return 1; } @@ -172,7 +184,7 @@ class TrashCan $counts = []; /** @var Entity $instance */ - foreach ((new EntityProvider)->all() as $key => $instance) { + foreach ((new EntityProvider())->all() as $key => $instance) { $counts[$key] = $instance->newQuery()->onlyTrashed()->count(); } @@ -181,6 +193,7 @@ class TrashCan /** * Destroy all items that have pending deletions. + * * @throws Exception */ public function empty(): int @@ -190,11 +203,13 @@ class TrashCan foreach ($deletions as $deletion) { $deleteCount += $this->destroyFromDeletion($deletion); } + return $deleteCount; } /** * Destroy an element from the given deletion model. + * * @throws Exception */ public function destroyFromDeletion(Deletion $deletion): int @@ -207,11 +222,13 @@ class TrashCan $count = $this->destroyEntity($deletion->deletable); } $deletion->delete(); + return $count; } /** * Restore the content within the given deletion. + * * @throws Exception */ public function restoreFromDeletion(Deletion $deletion): int @@ -229,6 +246,7 @@ class TrashCan } $deletion->delete(); + return $restoreCount; } @@ -236,6 +254,7 @@ class TrashCan * Automatically clear old content from the recycle bin * depending on the configured lifetime. * Returns the total number of deleted elements. + * * @throws Exception */ public function autoClearOld(): int @@ -287,6 +306,7 @@ class TrashCan /** * Destroy the given entity. + * * @throws Exception */ protected function destroyEntity(Entity $entity): int diff --git a/app/Exceptions/ApiAuthException.php b/app/Exceptions/ApiAuthException.php index 36ea8be9d..360370de4 100644 --- a/app/Exceptions/ApiAuthException.php +++ b/app/Exceptions/ApiAuthException.php @@ -4,5 +4,4 @@ namespace BookStack\Exceptions; class ApiAuthException extends UnauthorizedException { - } diff --git a/app/Exceptions/ConfirmationEmailException.php b/app/Exceptions/ConfirmationEmailException.php index 71407b3c0..c39d4f592 100644 --- a/app/Exceptions/ConfirmationEmailException.php +++ b/app/Exceptions/ConfirmationEmailException.php @@ -1,6 +1,7 @@ - [ 'message' => $e->getMessage(), - ] + ], ]; if ($e instanceof ValidationException) { @@ -92,14 +94,16 @@ class Handler extends ExceptionHandler } $responseData['error']['code'] = $code; + return new JsonResponse($responseData, $code, $headers); } /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Auth\AuthenticationException $exception + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Auth\AuthenticationException $exception + * * @return \Illuminate\Http\Response */ protected function unauthenticated($request, AuthenticationException $exception) @@ -114,8 +118,9 @@ class Handler extends ExceptionHandler /** * Convert a validation exception into a JSON response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Validation\ValidationException $exception + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Validation\ValidationException $exception + * * @return \Illuminate\Http\JsonResponse */ protected function invalidJson($request, ValidationException $exception) diff --git a/app/Exceptions/HttpFetchException.php b/app/Exceptions/HttpFetchException.php index 2a34bbc62..4ad45d92a 100644 --- a/app/Exceptions/HttpFetchException.php +++ b/app/Exceptions/HttpFetchException.php @@ -1,4 +1,6 @@ -message = $message; $this->redirectLocation = $redirectLocation; @@ -20,6 +22,7 @@ class NotifyException extends Exception implements Responsable /** * Send the response for this type of exception. + * * @inheritdoc */ public function toResponse($request) diff --git a/app/Exceptions/PermissionsException.php b/app/Exceptions/PermissionsException.php index e2a8c53b4..64da55d21 100644 --- a/app/Exceptions/PermissionsException.php +++ b/app/Exceptions/PermissionsException.php @@ -1,8 +1,9 @@ -getCode() === 0) ? 500 : $this->getCode(); + return response()->view('errors.' . $code, [ - 'message' => $this->getMessage(), + 'message' => $this->getMessage(), 'subtitle' => $this->subtitle, - 'details' => $this->details, + 'details' => $this->details, ], $code); } public function setSubtitle(string $subtitle): self { $this->subtitle = $subtitle; + return $this; } public function setDetails(string $details): self { $this->details = $details; + return $this; } } diff --git a/app/Exceptions/SamlException.php b/app/Exceptions/SamlException.php index 13db23f27..417fe1c66 100644 --- a/app/Exceptions/SamlException.php +++ b/app/Exceptions/SamlException.php @@ -1,6 +1,7 @@ -user = $user; + $this->loginService = $loginService; + parent::__construct(); + } + + /** + * @inheritdoc + */ + public function toResponse($request) + { + $redirect = '/login'; + + if ($this->loginService->awaitingEmailConfirmation($this->user)) { + return $this->awaitingEmailConfirmationResponse($request); + } + + if ($this->loginService->needsMfaVerification($this->user)) { + $redirect = '/mfa/verify'; + } + + return redirect($redirect); + } + + /** + * Provide an error response for when the current user's email is not confirmed + * in a system which requires it. + */ + protected function awaitingEmailConfirmationResponse(Request $request) + { + if ($request->wantsJson()) { + return response()->json([ + 'error' => [ + 'code' => 401, + 'message' => trans('errors.email_confirmation_awaiting'), + ], + ], 401); + } + + if (session()->get('sent-email-confirmation') === true) { + return redirect('/register/confirm'); + } + + return redirect('/register/confirm/awaiting'); + } +} diff --git a/app/Exceptions/UnauthorizedException.php b/app/Exceptions/UnauthorizedException.php index a13ba3a55..5c73ca02c 100644 --- a/app/Exceptions/UnauthorizedException.php +++ b/app/Exceptions/UnauthorizedException.php @@ -6,7 +6,6 @@ use Exception; class UnauthorizedException extends Exception { - /** * ApiAuthException constructor. */ diff --git a/app/Exceptions/UserRegistrationException.php b/app/Exceptions/UserRegistrationException.php index 953abb96d..e7ddb81c2 100644 --- a/app/Exceptions/UserRegistrationException.php +++ b/app/Exceptions/UserRegistrationException.php @@ -1,6 +1,7 @@ -toResponse(); } diff --git a/app/Http/Controllers/Api/ApiDocsController.php b/app/Http/Controllers/Api/ApiDocsController.php index c63ca698c..a1453e768 100644 --- a/app/Http/Controllers/Api/ApiDocsController.php +++ b/app/Http/Controllers/Api/ApiDocsController.php @@ -1,10 +1,11 @@ -setPageTitle(trans('settings.users_api_tokens_docs')); + return view('api-docs.index', [ 'docs' => $docs, ]); @@ -23,6 +25,7 @@ class ApiDocsController extends ApiController public function json() { $docs = ApiDocsGenerator::generateConsideringCache(); + return response()->json($docs); } } diff --git a/app/Http/Controllers/Api/BookApiController.php b/app/Http/Controllers/Api/BookApiController.php index 81ac9c7aa..abe23f45d 100644 --- a/app/Http/Controllers/Api/BookApiController.php +++ b/app/Http/Controllers/Api/BookApiController.php @@ -1,27 +1,26 @@ - [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'tags' => 'array', + 'tags' => 'array', ], 'update' => [ - 'name' => 'string|min:1|max:255', + 'name' => 'string|min:1|max:255', 'description' => 'string|max:1000', - 'tags' => 'array', + 'tags' => 'array', ], ]; @@ -36,6 +35,7 @@ class BookApiController extends ApiController public function list() { $books = Book::visible(); + return $this->apiListingResponse($books, [ 'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id', ]); @@ -43,6 +43,7 @@ class BookApiController extends ApiController /** * Create a new book in the system. + * * @throws ValidationException */ public function create(Request $request) @@ -51,6 +52,7 @@ class BookApiController extends ApiController $requestData = $this->validate($request, $this->rules['create']); $book = $this->bookRepo->create($requestData); + return response()->json($book); } @@ -60,11 +62,13 @@ class BookApiController extends ApiController public function read(string $id) { $book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($id); + return response()->json($book); } /** * Update the details of a single book. + * * @throws ValidationException */ public function update(Request $request, string $id) @@ -81,6 +85,7 @@ class BookApiController extends ApiController /** * Delete a single book. * This will typically send the book to the recycle bin. + * * @throws \Exception */ public function delete(string $id) @@ -89,6 +94,7 @@ class BookApiController extends ApiController $this->checkOwnablePermission('book-delete', $book); $this->bookRepo->destroy($book); + return response('', 204); } } diff --git a/app/Http/Controllers/Api/BookExportApiController.php b/app/Http/Controllers/Api/BookExportApiController.php index 3d813c4d4..028bc3a81 100644 --- a/app/Http/Controllers/Api/BookExportApiController.php +++ b/app/Http/Controllers/Api/BookExportApiController.php @@ -1,4 +1,6 @@ -exportFormatter = $exportFormatter; + $this->middleware('can:content-export'); } /** * Export a book as a PDF file. + * * @throws Throwable */ public function exportPdf(int $id) { $book = Book::visible()->findOrFail($id); $pdfContent = $this->exportFormatter->bookToPdf($book); + return $this->downloadResponse($pdfContent, $book->slug . '.pdf'); } /** * Export a book as a contained HTML file. + * * @throws Throwable */ public function exportHtml(int $id) { $book = Book::visible()->findOrFail($id); $htmlContent = $this->exportFormatter->bookToContainedHtml($book); + return $this->downloadResponse($htmlContent, $book->slug . '.html'); } @@ -42,6 +49,18 @@ class BookExportApiController extends ApiController { $book = Book::visible()->findOrFail($id); $textContent = $this->exportFormatter->bookToPlainText($book); + return $this->downloadResponse($textContent, $book->slug . '.txt'); } + + /** + * Export a book as a markdown file. + */ + public function exportMarkdown(int $id) + { + $book = Book::visible()->findOrFail($id); + $markdown = $this->exportFormatter->bookToMarkdown($book); + + return $this->downloadResponse($markdown, $book->slug . '.md'); + } } diff --git a/app/Http/Controllers/Api/BookshelfApiController.php b/app/Http/Controllers/Api/BookshelfApiController.php index 4ce93defa..c29e5b0ae 100644 --- a/app/Http/Controllers/Api/BookshelfApiController.php +++ b/app/Http/Controllers/Api/BookshelfApiController.php @@ -1,7 +1,9 @@ - [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'books' => 'array', + 'books' => 'array', ], 'update' => [ - 'name' => 'string|min:1|max:255', + 'name' => 'string|min:1|max:255', 'description' => 'string|max:1000', - 'books' => 'array', + 'books' => 'array', ], ]; @@ -42,6 +43,7 @@ class BookshelfApiController extends ApiController public function list() { $shelves = Bookshelf::visible(); + return $this->apiListingResponse($shelves, [ 'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id', ]); @@ -51,6 +53,7 @@ class BookshelfApiController extends ApiController * Create a new shelf in the system. * An array of books IDs can be provided in the request. These * will be added to the shelf in the same order as provided. + * * @throws ValidationException */ public function create(Request $request) @@ -73,8 +76,9 @@ class BookshelfApiController extends ApiController 'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy', 'books' => function (BelongsToMany $query) { $query->visible()->get(['id', 'name', 'slug']); - } + }, ])->findOrFail($id); + return response()->json($shelf); } @@ -83,6 +87,7 @@ class BookshelfApiController extends ApiController * An array of books IDs can be provided in the request. These * will be added to the shelf in the same order as provided and overwrite * any existing book assignments. + * * @throws ValidationException */ public function update(Request $request, string $id) @@ -94,14 +99,14 @@ class BookshelfApiController extends ApiController $bookIds = $request->get('books', null); $shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds); + return response()->json($shelf); } - - /** * Delete a single shelf. * This will typically send the shelf to the recycle bin. + * * @throws Exception */ public function delete(string $id) @@ -110,6 +115,7 @@ class BookshelfApiController extends ApiController $this->checkOwnablePermission('bookshelf-delete', $shelf); $this->bookshelfRepo->destroy($shelf); + return response('', 204); } } diff --git a/app/Http/Controllers/Api/ChapterApiController.php b/app/Http/Controllers/Api/ChapterApiController.php index e58c1c8e1..13b3f9821 100644 --- a/app/Http/Controllers/Api/ChapterApiController.php +++ b/app/Http/Controllers/Api/ChapterApiController.php @@ -1,10 +1,10 @@ - [ - 'book_id' => 'required|integer', - 'name' => 'required|string|max:255', + 'book_id' => 'required|integer', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'tags' => 'array', + 'tags' => 'array', ], 'update' => [ - 'book_id' => 'integer', - 'name' => 'string|min:1|max:255', + 'book_id' => 'integer', + 'name' => 'string|min:1|max:255', 'description' => 'string|max:1000', - 'tags' => 'array', + 'tags' => 'array', ], ]; @@ -41,6 +41,7 @@ class ChapterApiController extends ApiController public function list() { $chapters = Chapter::visible(); + return $this->apiListingResponse($chapters, [ 'id', 'book_id', 'name', 'slug', 'description', 'priority', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', @@ -59,6 +60,7 @@ class ChapterApiController extends ApiController $this->checkOwnablePermission('chapter-create', $book); $chapter = $this->chapterRepo->create($request->all(), $book); + return response()->json($chapter->load(['tags'])); } @@ -70,6 +72,7 @@ class ChapterApiController extends ApiController $chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) { $query->visible()->get(['id', 'name', 'slug']); }])->findOrFail($id); + return response()->json($chapter); } @@ -82,6 +85,7 @@ class ChapterApiController extends ApiController $this->checkOwnablePermission('chapter-update', $chapter); $updatedChapter = $this->chapterRepo->update($chapter, $request->all()); + return response()->json($updatedChapter->load(['tags'])); } @@ -95,6 +99,7 @@ class ChapterApiController extends ApiController $this->checkOwnablePermission('chapter-delete', $chapter); $this->chapterRepo->destroy($chapter); + return response('', 204); } } diff --git a/app/Http/Controllers/Api/ChapterExportApiController.php b/app/Http/Controllers/Api/ChapterExportApiController.php index afdfe555d..5715ab2e3 100644 --- a/app/Http/Controllers/Api/ChapterExportApiController.php +++ b/app/Http/Controllers/Api/ChapterExportApiController.php @@ -1,8 +1,9 @@ -exportFormatter = $exportFormatter; + $this->middleware('can:content-export'); } /** * Export a chapter as a PDF file. + * * @throws Throwable */ public function exportPdf(int $id) { $chapter = Chapter::visible()->findOrFail($id); $pdfContent = $this->exportFormatter->chapterToPdf($chapter); + return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf'); } /** * Export a chapter as a contained HTML file. + * * @throws Throwable */ public function exportHtml(int $id) { $chapter = Chapter::visible()->findOrFail($id); $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter); + return $this->downloadResponse($htmlContent, $chapter->slug . '.html'); } @@ -46,6 +52,18 @@ class ChapterExportApiController extends ApiController { $chapter = Chapter::visible()->findOrFail($id); $textContent = $this->exportFormatter->chapterToPlainText($chapter); + return $this->downloadResponse($textContent, $chapter->slug . '.txt'); } + + /** + * Export a chapter as a markdown file. + */ + public function exportMarkdown(int $id) + { + $chapter = Chapter::visible()->findOrFail($id); + $markdown = $this->exportFormatter->chapterToMarkdown($chapter); + + return $this->downloadResponse($markdown, $chapter->slug . '.md'); + } } diff --git a/app/Http/Controllers/Api/PageApiController.php b/app/Http/Controllers/Api/PageApiController.php index fd4a16eff..f698627a7 100644 --- a/app/Http/Controllers/Api/PageApiController.php +++ b/app/Http/Controllers/Api/PageApiController.php @@ -16,20 +16,20 @@ class PageApiController extends ApiController protected $rules = [ 'create' => [ - 'book_id' => 'required_without:chapter_id|integer', + 'book_id' => 'required_without:chapter_id|integer', 'chapter_id' => 'required_without:book_id|integer', - 'name' => 'required|string|max:255', - 'html' => 'required_without:markdown|string', - 'markdown' => 'required_without:html|string', - 'tags' => 'array', + 'name' => 'required|string|max:255', + 'html' => 'required_without:markdown|string', + 'markdown' => 'required_without:html|string', + 'tags' => 'array', ], 'update' => [ - 'book_id' => 'required|integer', + 'book_id' => 'required|integer', 'chapter_id' => 'required|integer', - 'name' => 'string|min:1|max:255', - 'html' => 'string', - 'markdown' => 'string', - 'tags' => 'array', + 'name' => 'string|min:1|max:255', + 'html' => 'string', + 'markdown' => 'string', + 'tags' => 'array', ], ]; @@ -44,6 +44,7 @@ class PageApiController extends ApiController public function list() { $pages = Page::visible(); + return $this->apiListingResponse($pages, [ 'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority', 'draft', 'template', @@ -89,6 +90,7 @@ class PageApiController extends ApiController public function read(string $id) { $page = $this->pageRepo->getById($id, []); + return response()->json($page->forJsonDisplay()); } @@ -107,12 +109,13 @@ class PageApiController extends ApiController $parent = null; if ($request->has('chapter_id')) { $parent = Chapter::visible()->findOrFail($request->get('chapter_id')); - } else if ($request->has('book_id')) { + } elseif ($request->has('book_id')) { $parent = Book::visible()->findOrFail($request->get('book_id')); } if ($parent && !$parent->matches($page->getParent())) { $this->checkOwnablePermission('page-delete', $page); + try { $this->pageRepo->move($page, $parent->getType() . ':' . $parent->id); } catch (Exception $exception) { @@ -125,6 +128,7 @@ class PageApiController extends ApiController } $updatedPage = $this->pageRepo->update($page, $request->all()); + return response()->json($updatedPage->forJsonDisplay()); } @@ -138,6 +142,7 @@ class PageApiController extends ApiController $this->checkOwnablePermission('page-delete', $page); $this->pageRepo->destroy($page); + return response('', 204); } } diff --git a/app/Http/Controllers/Api/PageExportApiController.php b/app/Http/Controllers/Api/PageExportApiController.php index 7563092cb..ce5700c79 100644 --- a/app/Http/Controllers/Api/PageExportApiController.php +++ b/app/Http/Controllers/Api/PageExportApiController.php @@ -1,4 +1,6 @@ -exportFormatter = $exportFormatter; + $this->middleware('can:content-export'); } /** * Export a page as a PDF file. + * * @throws Throwable */ public function exportPdf(int $id) { $page = Page::visible()->findOrFail($id); $pdfContent = $this->exportFormatter->pageToPdf($page); + return $this->downloadResponse($pdfContent, $page->slug . '.pdf'); } /** * Export a page as a contained HTML file. + * * @throws Throwable */ public function exportHtml(int $id) { $page = Page::visible()->findOrFail($id); $htmlContent = $this->exportFormatter->pageToContainedHtml($page); + return $this->downloadResponse($htmlContent, $page->slug . '.html'); } @@ -42,6 +49,18 @@ class PageExportApiController extends ApiController { $page = Page::visible()->findOrFail($id); $textContent = $this->exportFormatter->pageToPlainText($page); + return $this->downloadResponse($textContent, $page->slug . '.txt'); } + + /** + * Export a page as a markdown file. + */ + public function exportMarkdown(int $id) + { + $page = Page::visible()->findOrFail($id); + $markdown = $this->exportFormatter->pageToMarkdown($page); + + return $this->downloadResponse($markdown, $page->slug . '.md'); + } } diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index be20cda93..046b8c19d 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -1,4 +1,6 @@ -pageRepo = $pageRepo; } - /** * Endpoint at which attachments are uploaded to. + * * @throws ValidationException * @throws NotFoundException */ @@ -35,7 +37,7 @@ class AttachmentController extends Controller { $this->validate($request, [ 'uploaded_to' => 'required|integer|exists:pages,id', - 'file' => 'required|file' + 'file' => 'required|file', ]); $pageId = $request->get('uploaded_to'); @@ -57,12 +59,13 @@ class AttachmentController extends Controller /** * Update an uploaded attachment. + * * @throws ValidationException */ public function uploadUpdate(Request $request, $attachmentId) { $this->validate($request, [ - 'file' => 'required|file' + 'file' => 'required|file', ]); $attachment = Attachment::query()->findOrFail($attachmentId); @@ -83,6 +86,7 @@ class AttachmentController extends Controller /** * Get the update form for an attachment. + * * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function getUpdateForm(string $attachmentId) @@ -104,15 +108,16 @@ class AttachmentController extends Controller { /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); + try { $this->validate($request, [ 'attachment_edit_name' => 'required|string|min:1|max:255', - 'attachment_edit_url' => 'string|min:1|max:255|safe_url' + 'attachment_edit_url' => 'string|min:1|max:255|safe_url', ]); } catch (ValidationException $exception) { return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [ 'attachment' => $attachment, - 'errors' => new MessageBag($exception->errors()), + 'errors' => new MessageBag($exception->errors()), ]), 422); } @@ -132,6 +137,7 @@ class AttachmentController extends Controller /** * Attach a link to a page. + * * @throws NotFoundException */ public function attachLink(Request $request) @@ -141,8 +147,8 @@ class AttachmentController extends Controller try { $this->validate($request, [ 'attachment_link_uploaded_to' => 'required|integer|exists:pages,id', - 'attachment_link_name' => 'required|string|min:1|max:255', - 'attachment_link_url' => 'required|string|min:1|max:255|safe_url' + 'attachment_link_name' => 'required|string|min:1|max:255', + 'attachment_link_url' => 'required|string|min:1|max:255|safe_url', ]); } catch (ValidationException $exception) { return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [ @@ -172,6 +178,7 @@ class AttachmentController extends Controller { $page = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-view', $page); + return view('attachments.manager-list', [ 'attachments' => $page->attachments->all(), ]); @@ -179,6 +186,7 @@ class AttachmentController extends Controller /** * Update the attachment sorting. + * * @throws ValidationException * @throws NotFoundException */ @@ -192,11 +200,13 @@ class AttachmentController extends Controller $attachmentOrder = $request->get('order'); $this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId); + return response()->json(['message' => trans('entities.attachments_order_updated')]); } /** * Get an attachment from storage. + * * @throws FileNotFoundException * @throws NotFoundException */ @@ -204,6 +214,7 @@ class AttachmentController extends Controller { /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); + try { $page = $this->pageRepo->getById($attachment->uploaded_to); } catch (NotFoundException $exception) { @@ -222,11 +233,13 @@ class AttachmentController extends Controller if ($request->get('open') === 'true') { return $this->inlineDownloadResponse($attachmentContents, $fileName); } + return $this->downloadResponse($attachmentContents, $fileName); } /** * Delete a specific attachment in the system. + * * @throws Exception */ public function delete(string $attachmentId) @@ -235,6 +248,7 @@ class AttachmentController extends Controller $attachment = Attachment::query()->findOrFail($attachmentId); $this->checkOwnablePermission('attachment-delete', $attachment); $this->attachmentService->deleteFile($attachment); + return response()->json(['message' => trans('entities.attachments_deleted')]); } } diff --git a/app/Http/Controllers/AuditLogController.php b/app/Http/Controllers/AuditLogController.php index f73ee4a20..11efbfc23 100644 --- a/app/Http/Controllers/AuditLogController.php +++ b/app/Http/Controllers/AuditLogController.php @@ -8,19 +8,18 @@ use Illuminate\Support\Facades\DB; class AuditLogController extends Controller { - public function index(Request $request) { $this->checkPermission('settings-manage'); $this->checkPermission('users-manage'); $listDetails = [ - 'order' => $request->get('order', 'desc'), - 'event' => $request->get('event', ''), - 'sort' => $request->get('sort', 'created_at'), + 'order' => $request->get('order', 'desc'), + 'event' => $request->get('event', ''), + 'sort' => $request->get('sort', 'created_at'), 'date_from' => $request->get('date_from', ''), - 'date_to' => $request->get('date_to', ''), - 'user' => $request->get('user', ''), + 'date_to' => $request->get('date_to', ''), + 'user' => $request->get('user', ''), ]; $query = Activity::query() @@ -28,7 +27,7 @@ class AuditLogController extends Controller 'entity' => function ($query) { $query->withTrashed(); }, - 'user' + 'user', ]) ->orderBy($listDetails['sort'], $listDetails['order']); @@ -51,9 +50,10 @@ class AuditLogController extends Controller $types = DB::table('activities')->select('type')->distinct()->pluck('type'); $this->setPageTitle(trans('settings.audit')); + return view('settings.audit', [ - 'activities' => $activities, - 'listDetails' => $listDetails, + 'activities' => $activities, + 'listDetails' => $listDetails, 'activityTypes' => $types, ]); } diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php index 6e6a0e779..02b9ef276 100644 --- a/app/Http/Controllers/Auth/ConfirmEmailController.php +++ b/app/Http/Controllers/Auth/ConfirmEmailController.php @@ -2,15 +2,13 @@ namespace BookStack\Http\Controllers\Auth; -use BookStack\Actions\ActivityType; use BookStack\Auth\Access\EmailConfirmationService; +use BookStack\Auth\Access\LoginService; use BookStack\Auth\UserRepo; use BookStack\Exceptions\ConfirmationEmailException; use BookStack\Exceptions\UserTokenExpiredException; use BookStack\Exceptions\UserTokenNotFoundException; -use BookStack\Facades\Theme; use BookStack\Http\Controllers\Controller; -use BookStack\Theming\ThemeEvents; use Exception; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -20,18 +18,22 @@ use Illuminate\View\View; class ConfirmEmailController extends Controller { protected $emailConfirmationService; + protected $loginService; protected $userRepo; /** * Create a new controller instance. */ - public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) - { + public function __construct( + EmailConfirmationService $emailConfirmationService, + LoginService $loginService, + UserRepo $userRepo + ) { $this->emailConfirmationService = $emailConfirmationService; + $this->loginService = $loginService; $this->userRepo = $userRepo; } - /** * Show the page to tell the user to check their email * and confirm their address. @@ -44,19 +46,23 @@ class ConfirmEmailController extends Controller /** * Shows a notice that a user's email address has not been confirmed, * Also has the option to re-send the confirmation email. - * @return View */ public function showAwaiting() { - return view('auth.user-unconfirmed'); + $user = $this->loginService->getLastLoginAttemptUser(); + + return view('auth.user-unconfirmed', ['user' => $user]); } /** * Confirms an email via a token and logs the user into the system. + * * @param $token - * @return RedirectResponse|Redirector + * * @throws ConfirmationEmailException * @throws Exception + * + * @return RedirectResponse|Redirector */ public function confirm($token) { @@ -65,6 +71,7 @@ class ConfirmEmailController extends Controller } catch (Exception $exception) { if ($exception instanceof UserTokenNotFoundException) { $this->showErrorNotification(trans('errors.email_confirmation_invalid')); + return redirect('/register'); } @@ -72,6 +79,7 @@ class ConfirmEmailController extends Controller $user = $this->userRepo->getById($exception->userId); $this->emailConfirmationService->sendConfirmation($user); $this->showErrorNotification(trans('errors.email_confirmation_expired')); + return redirect('/register/confirm'); } @@ -82,25 +90,24 @@ class ConfirmEmailController extends Controller $user->email_confirmed = true; $user->save(); - auth()->login($user); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user); - $this->logActivity(ActivityType::AUTH_LOGIN, $user); - $this->showSuccessNotification(trans('auth.email_confirm_success')); $this->emailConfirmationService->deleteByUser($user); + $this->showSuccessNotification(trans('auth.email_confirm_success')); + $this->loginService->login($user, auth()->getDefaultDriver()); return redirect('/'); } - /** - * Resend the confirmation email + * Resend the confirmation email. + * * @param Request $request + * * @return View */ public function resend(Request $request) { $this->validate($request, [ - 'email' => 'required|email|exists:users,email' + 'email' => 'required|email|exists:users,email', ]); $user = $this->userRepo->getByEmail($request->get('email')); @@ -108,10 +115,12 @@ class ConfirmEmailController extends Controller $this->emailConfirmationService->sendConfirmation($user); } catch (Exception $e) { $this->showErrorNotification(trans('auth.email_confirm_send_error')); + return redirect('/register/confirm'); } $this->showSuccessNotification(trans('auth.email_confirm_resent')); + return redirect('/register/confirm'); } } diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 5a033c6aa..3df0608f8 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -34,11 +34,11 @@ class ForgotPasswordController extends Controller $this->middleware('guard:standard'); } - /** * Send a reset link to the given user. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\RedirectResponse */ public function sendResetLinkEmail(Request $request) @@ -59,6 +59,7 @@ class ForgotPasswordController extends Controller if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) { $message = trans('auth.reset_password_sent', ['email' => $request->get('email')]); $this->showSuccessNotification($message); + return back()->with('status', trans($response)); } diff --git a/app/Http/Controllers/Auth/HandlesPartialLogins.php b/app/Http/Controllers/Auth/HandlesPartialLogins.php new file mode 100644 index 000000000..c7f362151 --- /dev/null +++ b/app/Http/Controllers/Auth/HandlesPartialLogins.php @@ -0,0 +1,25 @@ +make(LoginService::class); + $user = auth()->user() ?? $loginService->getLastLoginAttemptUser(); + + if (!$user) { + throw new NotFoundException('A user for this action could not be found'); + } + + return $user; + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 16f7fc010..7c8eb2c86 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -3,13 +3,11 @@ namespace BookStack\Http\Controllers\Auth; use Activity; -use BookStack\Actions\ActivityType; +use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\SocialAuthService; use BookStack\Exceptions\LoginAttemptEmailNeededException; use BookStack\Exceptions\LoginAttemptException; -use BookStack\Facades\Theme; use BookStack\Http\Controllers\Controller; -use BookStack\Theming\ThemeEvents; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -30,23 +28,26 @@ class LoginController extends Controller use AuthenticatesUsers; /** - * Redirection paths + * Redirection paths. */ protected $redirectTo = '/'; protected $redirectPath = '/'; protected $redirectAfterLogout = '/login'; protected $socialAuthService; + protected $loginService; /** * Create a new controller instance. */ - public function __construct(SocialAuthService $socialAuthService) + public function __construct(SocialAuthService $socialAuthService, LoginService $loginService) { $this->middleware('guest', ['only' => ['getLogin', 'login']]); $this->middleware('guard:standard,ldap', ['only' => ['login', 'logout']]); $this->socialAuthService = $socialAuthService; + $this->loginService = $loginService; + $this->redirectPath = url('/'); $this->redirectAfterLogout = url('/login'); } @@ -74,33 +75,28 @@ class LoginController extends Controller if ($request->has('email')) { session()->flashInput([ - 'email' => $request->get('email'), - 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '' + 'email' => $request->get('email'), + 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '', ]); } // Store the previous location for redirect after login - $previous = url()->previous(''); - if ($previous && $previous !== url('/login') && setting('app-public')) { - $isPreviousFromInstance = (strpos($previous, url('/')) === 0); - if ($isPreviousFromInstance) { - redirect()->setIntendedUrl($previous); - } - } + $this->updateIntendedFromPrevious(); return view('auth.login', [ - 'socialDrivers' => $socialDrivers, - 'authMethod' => $authMethod, + 'socialDrivers' => $socialDrivers, + 'authMethod' => $authMethod, ]); } /** * Handle a login request to the application. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse + * @param \Illuminate\Http\Request $request * * @throws \Illuminate\Validation\ValidationException + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function login(Request $request) { @@ -115,6 +111,7 @@ class LoginController extends Controller $this->fireLockoutEvent($request); Activity::logFailedLogin($username); + return $this->sendLockoutResponse($request); } @@ -124,6 +121,7 @@ class LoginController extends Controller } } catch (LoginAttemptException $exception) { Activity::logFailedLogin($username); + return $this->sendLoginAttemptExceptionResponse($exception, $request); } @@ -133,38 +131,47 @@ class LoginController extends Controller $this->incrementLoginAttempts($request); Activity::logFailedLogin($username); + return $this->sendFailedLoginResponse($request); } + /** + * Attempt to log the user into the application. + * + * @param \Illuminate\Http\Request $request + * + * @return bool + */ + protected function attemptLogin(Request $request) + { + return $this->loginService->attempt( + $this->credentials($request), + auth()->getDefaultDriver(), + $request->filled('remember') + ); + } + /** * The user has been authenticated. * - * @param \Illuminate\Http\Request $request - * @param mixed $user + * @param \Illuminate\Http\Request $request + * @param mixed $user + * * @return mixed */ protected function authenticated(Request $request, $user) { - // Authenticate on all session guards if a likely admin - if ($user->can('users-manage') && $user->can('user-roles-manage')) { - $guards = ['standard', 'ldap', 'saml2']; - foreach ($guards as $guard) { - auth($guard)->login($user); - } - } - - Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user); - $this->logActivity(ActivityType::AUTH_LOGIN, $user); return redirect()->intended($this->redirectPath()); } /** * Validate the user login request. * - * @param \Illuminate\Http\Request $request - * @return void + * @param \Illuminate\Http\Request $request * * @throws \Illuminate\Validation\ValidationException + * + * @return void */ protected function validateLogin(Request $request) { @@ -203,10 +210,11 @@ class LoginController extends Controller /** * Get the failed login response instance. * - * @param \Illuminate\Http\Request $request - * @return \Symfony\Component\HttpFoundation\Response + * @param \Illuminate\Http\Request $request * * @throws \Illuminate\Validation\ValidationException + * + * @return \Symfony\Component\HttpFoundation\Response */ protected function sendFailedLoginResponse(Request $request) { @@ -214,4 +222,32 @@ class LoginController extends Controller $this->username() => [trans('auth.failed')], ])->redirectTo('/login'); } + + /** + * Update the intended URL location from their previous URL. + * Ignores if not from the current app instance or if from certain + * login or authentication routes. + */ + protected function updateIntendedFromPrevious(): void + { + // Store the previous location for redirect after login + $previous = url()->previous(''); + $isPreviousFromInstance = (strpos($previous, url('/')) === 0); + if (!$previous || !setting('app-public') || !$isPreviousFromInstance) { + return; + } + + $ignorePrefixList = [ + '/login', + '/mfa', + ]; + + foreach ($ignorePrefixList as $ignorePrefix) { + if (strpos($previous, url($ignorePrefix)) === 0) { + return; + } + } + + redirect()->setIntendedUrl($previous); + } } diff --git a/app/Http/Controllers/Auth/MfaBackupCodesController.php b/app/Http/Controllers/Auth/MfaBackupCodesController.php new file mode 100644 index 000000000..d92029bf1 --- /dev/null +++ b/app/Http/Controllers/Auth/MfaBackupCodesController.php @@ -0,0 +1,98 @@ +generateNewSet(); + session()->put(self::SETUP_SECRET_SESSION_KEY, encrypt($codes)); + + $downloadUrl = 'data:application/octet-stream;base64,' . base64_encode(implode("\n\n", $codes)); + + return view('mfa.backup-codes-generate', [ + 'codes' => $codes, + 'downloadUrl' => $downloadUrl, + ]); + } + + /** + * Confirm the setup of backup codes, storing them against the user. + * + * @throws Exception + */ + public function confirm() + { + if (!session()->has(self::SETUP_SECRET_SESSION_KEY)) { + return response('No generated codes found in the session', 500); + } + + $codes = decrypt(session()->pull(self::SETUP_SECRET_SESSION_KEY)); + MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_BACKUP_CODES, json_encode($codes)); + + $this->logActivity(ActivityType::MFA_SETUP_METHOD, 'backup-codes'); + + if (!auth()->check()) { + $this->showSuccessNotification(trans('auth.mfa_setup_login_notification')); + + return redirect('/login'); + } + + return redirect('/mfa/setup'); + } + + /** + * Verify the MFA method submission on check. + * + * @throws NotFoundException + * @throws ValidationException + */ + public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService) + { + $user = $this->currentOrLastAttemptedUser(); + $codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]'; + + $this->validate($request, [ + 'code' => [ + 'required', + 'max:12', 'min:8', + function ($attribute, $value, $fail) use ($codeService, $codes) { + if (!$codeService->inputCodeExistsInSet($value, $codes)) { + $fail(trans('validation.backup_codes')); + } + }, + ], + ]); + + $updatedCodes = $codeService->removeInputCodeFromSet($request->get('code'), $codes); + MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes); + + $mfaSession->markVerifiedForUser($user); + $loginService->reattemptLoginFor($user); + + if ($codeService->countCodesInSet($updatedCodes) < 5) { + $this->showWarningNotification(trans('auth.mfa_backup_codes_usage_limit_warning')); + } + + return redirect()->intended(); + } +} diff --git a/app/Http/Controllers/Auth/MfaController.php b/app/Http/Controllers/Auth/MfaController.php new file mode 100644 index 000000000..ca77cc708 --- /dev/null +++ b/app/Http/Controllers/Auth/MfaController.php @@ -0,0 +1,71 @@ +currentOrLastAttemptedUser() + ->mfaValues() + ->get(['id', 'method']) + ->groupBy('method'); + + return view('mfa.setup', [ + 'userMethods' => $userMethods, + ]); + } + + /** + * Remove an MFA method for the current user. + * + * @throws \Exception + */ + public function remove(string $method) + { + if (in_array($method, MfaValue::allMethods())) { + $value = user()->mfaValues()->where('method', '=', $method)->first(); + if ($value) { + $value->delete(); + $this->logActivity(ActivityType::MFA_REMOVE_METHOD, $method); + } + } + + return redirect('/mfa/setup'); + } + + /** + * Show the page to start an MFA verification. + */ + public function verify(Request $request) + { + $desiredMethod = $request->get('method'); + $userMethods = $this->currentOrLastAttemptedUser() + ->mfaValues() + ->get(['id', 'method']) + ->groupBy('method'); + + // Basic search for the default option for a user. + // (Prioritises totp over backup codes) + $method = $userMethods->has($desiredMethod) ? $desiredMethod : $userMethods->keys()->sort()->reverse()->first(); + $otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) { + return $method !== $userMethod; + })->all(); + + return view('mfa.verify', [ + 'userMethods' => $userMethods, + 'method' => $method, + 'otherMethods' => $otherMethods, + ]); + } +} diff --git a/app/Http/Controllers/Auth/MfaTotpController.php b/app/Http/Controllers/Auth/MfaTotpController.php new file mode 100644 index 000000000..5a932d6e9 --- /dev/null +++ b/app/Http/Controllers/Auth/MfaTotpController.php @@ -0,0 +1,97 @@ +has(static::SETUP_SECRET_SESSION_KEY)) { + $totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY)); + } else { + $totpSecret = $totp->generateSecret(); + session()->put(static::SETUP_SECRET_SESSION_KEY, encrypt($totpSecret)); + } + + $qrCodeUrl = $totp->generateUrl($totpSecret); + $svg = $totp->generateQrCodeSvg($qrCodeUrl); + + return view('mfa.totp-generate', [ + 'secret' => $totpSecret, + 'svg' => $svg, + ]); + } + + /** + * Confirm the setup of TOTP and save the auth method secret + * against the current user. + * + * @throws ValidationException + * @throws NotFoundException + */ + public function confirm(Request $request) + { + $totpSecret = decrypt(session()->get(static::SETUP_SECRET_SESSION_KEY)); + $this->validate($request, [ + 'code' => [ + 'required', + 'max:12', 'min:4', + new TotpValidationRule($totpSecret), + ], + ]); + + MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_TOTP, $totpSecret); + session()->remove(static::SETUP_SECRET_SESSION_KEY); + $this->logActivity(ActivityType::MFA_SETUP_METHOD, 'totp'); + + if (!auth()->check()) { + $this->showSuccessNotification(trans('auth.mfa_setup_login_notification')); + + return redirect('/login'); + } + + return redirect('/mfa/setup'); + } + + /** + * Verify the MFA method submission on check. + * + * @throws NotFoundException + */ + public function verify(Request $request, LoginService $loginService, MfaSession $mfaSession) + { + $user = $this->currentOrLastAttemptedUser(); + $totpSecret = MfaValue::getValueForUser($user, MfaValue::METHOD_TOTP); + + $this->validate($request, [ + 'code' => [ + 'required', + 'max:12', 'min:4', + new TotpValidationRule($totpSecret), + ], + ]); + + $mfaSession->markVerifiedForUser($user); + $loginService->reattemptLoginFor($user); + + return redirect()->intended(); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 7d7d8732b..bd1ffeac2 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -2,14 +2,13 @@ namespace BookStack\Http\Controllers\Auth; -use BookStack\Actions\ActivityType; +use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\RegistrationService; use BookStack\Auth\Access\SocialAuthService; use BookStack\Auth\User; +use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Exceptions\UserRegistrationException; -use BookStack\Facades\Theme; use BookStack\Http\Controllers\Controller; -use BookStack\Theming\ThemeEvents; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -32,6 +31,7 @@ class RegisterController extends Controller protected $socialAuthService; protected $registrationService; + protected $loginService; /** * Where to redirect users after login / registration. @@ -44,13 +44,17 @@ class RegisterController extends Controller /** * Create a new controller instance. */ - public function __construct(SocialAuthService $socialAuthService, RegistrationService $registrationService) - { + public function __construct( + SocialAuthService $socialAuthService, + RegistrationService $registrationService, + LoginService $loginService + ) { $this->middleware('guest'); $this->middleware('guard:standard'); $this->socialAuthService = $socialAuthService; $this->registrationService = $registrationService; + $this->loginService = $loginService; $this->redirectTo = url('/'); $this->redirectPath = url('/'); @@ -64,20 +68,22 @@ class RegisterController extends Controller protected function validator(array $data) { return Validator::make($data, [ - 'name' => 'required|min:2|max:255', - 'email' => 'required|email|max:255|unique:users', + 'name' => 'required|min:2|max:255', + 'email' => 'required|email|max:255|unique:users', 'password' => 'required|min:8', ]); } /** * Show the application registration form. + * * @throws UserRegistrationException */ public function getRegister() { $this->registrationService->ensureRegistrationAllowed(); $socialDrivers = $this->socialAuthService->getActiveDrivers(); + return view('auth.register', [ 'socialDrivers' => $socialDrivers, ]); @@ -85,7 +91,9 @@ class RegisterController extends Controller /** * Handle a registration request for the application. + * * @throws UserRegistrationException + * @throws StoppedAuthenticationException */ public function postRegister(Request $request) { @@ -95,30 +103,32 @@ class RegisterController extends Controller try { $user = $this->registrationService->registerUser($userData); - auth()->login($user); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user); - $this->logActivity(ActivityType::AUTH_LOGIN, $user); + $this->loginService->login($user, auth()->getDefaultDriver()); } catch (UserRegistrationException $exception) { if ($exception->getMessage()) { $this->showErrorNotification($exception->getMessage()); } + return redirect($exception->redirectLocation); } $this->showSuccessNotification(trans('auth.register_success')); + return redirect($this->redirectPath()); } /** * Create a new user instance after a valid registration. - * @param array $data + * + * @param array $data + * * @return User */ protected function create(array $data) { return User::create([ - 'name' => $data['name'], - 'email' => $data['email'], + 'name' => $data['name'], + 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); } diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 59e9ab79b..a31529b11 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -40,7 +40,8 @@ class ResetPasswordController extends Controller * Get the response for a successful password reset. * * @param Request $request - * @param string $response + * @param string $response + * * @return \Illuminate\Http\Response */ protected function sendResetResponse(Request $request, $response) @@ -48,6 +49,7 @@ class ResetPasswordController extends Controller $message = trans('auth.reset_password_success'); $this->showSuccessNotification($message); $this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user()); + return redirect($this->redirectPath()) ->with('status', trans($response)); } @@ -55,8 +57,9 @@ class ResetPasswordController extends Controller /** * Get the response for a failed password reset. * - * @param \Illuminate\Http\Request $request - * @param string $response + * @param \Illuminate\Http\Request $request + * @param string $response + * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse */ protected function sendResetFailedResponse(Request $request, $response) diff --git a/app/Http/Controllers/Auth/Saml2Controller.php b/app/Http/Controllers/Auth/Saml2Controller.php index dc7814c4b..14eb65b71 100644 --- a/app/Http/Controllers/Auth/Saml2Controller.php +++ b/app/Http/Controllers/Auth/Saml2Controller.php @@ -7,7 +7,6 @@ use BookStack\Http\Controllers\Controller; class Saml2Controller extends Controller { - protected $samlService; /** @@ -50,8 +49,9 @@ class Saml2Controller extends Controller public function metadata() { $metaData = $this->samlService->metadata(); + return response()->make($metaData, 200, [ - 'Content-Type' => 'text/xml' + 'Content-Type' => 'text/xml', ]); } @@ -63,6 +63,7 @@ class Saml2Controller extends Controller { $requestId = session()->pull('saml2_logout_request_id', null); $redirect = $this->samlService->processSlsResponse($requestId) ?? '/'; + return redirect($redirect); } @@ -77,6 +78,7 @@ class Saml2Controller extends Controller $user = $this->samlService->processAcsResponse($requestId); if ($user === null) { $this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')])); + return redirect('/login'); } diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index 447f0afc9..1691668a2 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -2,48 +2,53 @@ namespace BookStack\Http\Controllers\Auth; -use BookStack\Actions\ActivityType; +use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\RegistrationService; use BookStack\Auth\Access\SocialAuthService; use BookStack\Exceptions\SocialDriverNotConfigured; use BookStack\Exceptions\SocialSignInAccountNotUsed; use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\UserRegistrationException; -use BookStack\Facades\Theme; use BookStack\Http\Controllers\Controller; -use BookStack\Theming\ThemeEvents; use Illuminate\Http\Request; use Illuminate\Support\Str; use Laravel\Socialite\Contracts\User as SocialUser; class SocialController extends Controller { - protected $socialAuthService; protected $registrationService; + protected $loginService; /** * SocialController constructor. */ - public function __construct(SocialAuthService $socialAuthService, RegistrationService $registrationService) - { + public function __construct( + SocialAuthService $socialAuthService, + RegistrationService $registrationService, + LoginService $loginService + ) { $this->middleware('guest')->only(['getRegister', 'postRegister']); $this->socialAuthService = $socialAuthService; $this->registrationService = $registrationService; + $this->loginService = $loginService; } /** * Redirect to the relevant social site. + * * @throws SocialDriverNotConfigured */ public function login(string $socialDriver) { session()->put('social-callback', 'login'); + return $this->socialAuthService->startLogIn($socialDriver); } /** * Redirect to the social site for authentication intended to register. + * * @throws SocialDriverNotConfigured * @throws UserRegistrationException */ @@ -51,11 +56,13 @@ class SocialController extends Controller { $this->registrationService->ensureRegistrationAllowed(); session()->put('social-callback', 'register'); + return $this->socialAuthService->startRegister($socialDriver); } /** * The callback for social login services. + * * @throws SocialSignInException * @throws SocialDriverNotConfigured * @throws UserRegistrationException @@ -70,7 +77,7 @@ class SocialController extends Controller if ($request->has('error') && $request->has('error_description')) { throw new SocialSignInException(trans('errors.social_login_bad_response', [ 'socialAccount' => $socialDriver, - 'error' => $request->get('error_description'), + 'error' => $request->get('error_description'), ]), '/login'); } @@ -85,6 +92,7 @@ class SocialController extends Controller if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) { return $this->socialRegisterCallback($socialDriver, $socialUser); } + throw $exception; } } @@ -103,11 +111,13 @@ class SocialController extends Controller { $this->socialAuthService->detachSocialAccount($socialDriver); session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)])); + return redirect(user()->getEditUrl()); } /** * Register a new user after a registration callback. + * * @throws UserRegistrationException */ protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser) @@ -118,9 +128,9 @@ class SocialController extends Controller // Create an array of the user data to create a new user instance $userData = [ - 'name' => $socialUser->getName(), - 'email' => $socialUser->getEmail(), - 'password' => Str::random(32) + 'name' => $socialUser->getName(), + 'email' => $socialUser->getEmail(), + 'password' => Str::random(32), ]; // Take name from email address if empty @@ -129,11 +139,9 @@ class SocialController extends Controller } $user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified); - auth()->login($user); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $user); - $this->logActivity(ActivityType::AUTH_LOGIN, $user); - $this->showSuccessNotification(trans('auth.register_success')); + $this->loginService->login($user, $socialDriver); + return redirect('/'); } } diff --git a/app/Http/Controllers/Auth/UserInviteController.php b/app/Http/Controllers/Auth/UserInviteController.php index ab7452248..bd1912b0b 100644 --- a/app/Http/Controllers/Auth/UserInviteController.php +++ b/app/Http/Controllers/Auth/UserInviteController.php @@ -2,14 +2,12 @@ namespace BookStack\Http\Controllers\Auth; -use BookStack\Actions\ActivityType; +use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\UserInviteService; use BookStack\Auth\UserRepo; use BookStack\Exceptions\UserTokenExpiredException; use BookStack\Exceptions\UserTokenNotFoundException; -use BookStack\Facades\Theme; use BookStack\Http\Controllers\Controller; -use BookStack\Theming\ThemeEvents; use Exception; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -18,22 +16,25 @@ use Illuminate\Routing\Redirector; class UserInviteController extends Controller { protected $inviteService; + protected $loginService; protected $userRepo; /** * Create a new controller instance. */ - public function __construct(UserInviteService $inviteService, UserRepo $userRepo) + public function __construct(UserInviteService $inviteService, LoginService $loginService, UserRepo $userRepo) { $this->middleware('guest'); $this->middleware('guard:standard'); $this->inviteService = $inviteService; + $this->loginService = $loginService; $this->userRepo = $userRepo; } /** * Show the page for the user to set the password for their account. + * * @throws Exception */ public function showSetPassword(string $token) @@ -51,12 +52,13 @@ class UserInviteController extends Controller /** * Sets the password for an invited user and then grants them access. + * * @throws Exception */ public function setPassword(Request $request, string $token) { $this->validate($request, [ - 'password' => 'required|min:8' + 'password' => 'required|min:8', ]); try { @@ -70,19 +72,19 @@ class UserInviteController extends Controller $user->email_confirmed = true; $user->save(); - auth()->login($user); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user); - $this->logActivity(ActivityType::AUTH_LOGIN, $user); - $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')])); $this->inviteService->deleteByUser($user); + $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')])); + $this->loginService->login($user, auth()->getDefaultDriver()); return redirect('/'); } /** * Check and validate the exception thrown when checking an invite token. - * @return RedirectResponse|Redirector + * * @throws Exception + * + * @return RedirectResponse|Redirector */ protected function handleTokenException(Exception $exception) { @@ -92,6 +94,7 @@ class UserInviteController extends Controller if ($exception instanceof UserTokenExpiredException) { $this->showErrorNotification(trans('errors.invite_token_expired')); + return redirect('/password/email'); } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 64ae982d5..7c099377c 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -1,13 +1,15 @@ -entityContextManager->clearShelfContext(); $this->setPageTitle(trans('entities.books')); + return view('books.index', [ - 'books' => $books, + 'books' => $books, 'recents' => $recents, 'popular' => $popular, - 'new' => $new, - 'view' => $view, - 'sort' => $sort, - 'order' => $order, + 'new' => $new, + 'view' => $view, + 'sort' => $sort, + 'order' => $order, ]); } @@ -67,13 +69,15 @@ class BookController extends Controller } $this->setPageTitle(trans('entities.books_create')); + return view('books.create', [ - 'bookshelf' => $bookshelf + 'bookshelf' => $bookshelf, ]); } /** * Store a newly created book in storage. + * * @throws ImageUploadException * @throws ValidationException */ @@ -81,9 +85,9 @@ class BookController extends Controller { $this->checkPermission('book-create-all'); $this->validate($request, [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'image' => 'nullable|' . $this->getImageValidationRules(), + 'image' => 'nullable|' . $this->getImageValidationRules(), ]); $bookshelf = null; @@ -118,12 +122,13 @@ class BookController extends Controller } $this->setPageTitle($book->getShortName()); + return view('books.show', [ - 'book' => $book, - 'current' => $book, - 'bookChildren' => $bookChildren, + 'book' => $book, + 'current' => $book, + 'bookChildren' => $bookChildren, 'bookParentShelves' => $bookParentShelves, - 'activity' => Activity::entityActivity($book, 20, 1) + 'activity' => Activity::entityActivity($book, 20, 1), ]); } @@ -135,11 +140,13 @@ class BookController extends Controller $book = $this->bookRepo->getBySlug($slug); $this->checkOwnablePermission('book-update', $book); $this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()])); + return view('books.edit', ['book' => $book, 'current' => $book]); } /** * Update the specified book in storage. + * * @throws ImageUploadException * @throws ValidationException * @throws Throwable @@ -149,9 +156,9 @@ class BookController extends Controller $book = $this->bookRepo->getBySlug($slug); $this->checkOwnablePermission('book-update', $book); $this->validate($request, [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'image' => 'nullable|' . $this->getImageValidationRules(), + 'image' => 'nullable|' . $this->getImageValidationRules(), ]); $book = $this->bookRepo->update($book, $request->all()); @@ -169,11 +176,13 @@ class BookController extends Controller $book = $this->bookRepo->getBySlug($bookSlug); $this->checkOwnablePermission('book-delete', $book); $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()])); + return view('books.delete', ['book' => $book, 'current' => $book]); } /** * Remove the specified book from the system. + * * @throws Throwable */ public function destroy(string $bookSlug) @@ -201,6 +210,7 @@ class BookController extends Controller /** * Set the restrictions for this book. + * * @throws Throwable */ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug) @@ -211,6 +221,7 @@ class BookController extends Controller $permissionsUpdater->updateFromPermissionsForm($book, $request); $this->showSuccessNotification(trans('entities.books_permissions_updated')); + return redirect($book->getUrl()); } } diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php index 1c1f12442..7f6dd8017 100644 --- a/app/Http/Controllers/BookExportController.php +++ b/app/Http/Controllers/BookExportController.php @@ -2,13 +2,12 @@ namespace BookStack\Http\Controllers; -use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Repos\BookRepo; +use BookStack\Entities\Tools\ExportFormatter; use Throwable; class BookExportController extends Controller { - protected $bookRepo; protected $exportFormatter; @@ -19,27 +18,32 @@ class BookExportController extends Controller { $this->bookRepo = $bookRepo; $this->exportFormatter = $exportFormatter; + $this->middleware('can:content-export'); } /** * Export a book as a PDF file. + * * @throws Throwable */ public function pdf(string $bookSlug) { $book = $this->bookRepo->getBySlug($bookSlug); $pdfContent = $this->exportFormatter->bookToPdf($book); + return $this->downloadResponse($pdfContent, $bookSlug . '.pdf'); } /** * Export a book as a contained HTML file. + * * @throws Throwable */ public function html(string $bookSlug) { $book = $this->bookRepo->getBySlug($bookSlug); $htmlContent = $this->exportFormatter->bookToContainedHtml($book); + return $this->downloadResponse($htmlContent, $bookSlug . '.html'); } @@ -50,6 +54,18 @@ class BookExportController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $textContent = $this->exportFormatter->bookToPlainText($book); + return $this->downloadResponse($textContent, $bookSlug . '.txt'); } + + /** + * Export a book as a markdown file. + */ + public function markdown(string $bookSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $textContent = $this->exportFormatter->bookToMarkdown($book); + + return $this->downloadResponse($textContent, $bookSlug . '.md'); + } } diff --git a/app/Http/Controllers/BookSortController.php b/app/Http/Controllers/BookSortController.php index 6d3199cbe..0bd394778 100644 --- a/app/Http/Controllers/BookSortController.php +++ b/app/Http/Controllers/BookSortController.php @@ -4,15 +4,14 @@ namespace BookStack\Http\Controllers; use BookStack\Actions\ActivityType; use BookStack\Entities\Models\Book; -use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Repos\BookRepo; +use BookStack\Entities\Tools\BookContents; use BookStack\Exceptions\SortOperationException; use BookStack\Facades\Activity; use Illuminate\Http\Request; class BookSortController extends Controller { - protected $bookRepo; public function __construct(BookRepo $bookRepo) @@ -31,6 +30,7 @@ class BookSortController extends Controller $bookChildren = (new BookContents($book))->getTree(false); $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()])); + return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]); } @@ -42,7 +42,8 @@ class BookSortController extends Controller { $book = $this->bookRepo->getBySlug($bookSlug); $bookChildren = (new BookContents($book))->getTree(); - return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); + + return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]); } /** diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index b4795db09..da16d7822 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -1,22 +1,22 @@ -getForCurrentUser('bookshelves_sort', 'name'); $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc'); $sortOptions = [ - 'name' => trans('common.sort_name'), + 'name' => trans('common.sort_name'), 'created_at' => trans('common.sort_created_at'), 'updated_at' => trans('common.sort_updated_at'), ]; @@ -49,14 +49,15 @@ class BookshelfController extends Controller $this->entityContextManager->clearShelfContext(); $this->setPageTitle(trans('entities.shelves')); + return view('shelves.index', [ - 'shelves' => $shelves, - 'recents' => $recents, - 'popular' => $popular, - 'new' => $new, - 'view' => $view, - 'sort' => $sort, - 'order' => $order, + 'shelves' => $shelves, + 'recents' => $recents, + 'popular' => $popular, + 'new' => $new, + 'view' => $view, + 'sort' => $sort, + 'order' => $order, 'sortOptions' => $sortOptions, ]); } @@ -69,11 +70,13 @@ class BookshelfController extends Controller $this->checkPermission('bookshelf-create-all'); $books = Book::hasPermission('update')->get(); $this->setPageTitle(trans('entities.shelves_create')); + return view('shelves.create', ['books' => $books]); } /** * Store a newly created bookshelf in storage. + * * @throws ValidationException * @throws ImageUploadException */ @@ -81,9 +84,9 @@ class BookshelfController extends Controller { $this->checkPermission('bookshelf-create-all'); $this->validate($request, [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'image' => 'nullable|' . $this->getImageValidationRules(), + 'image' => 'nullable|' . $this->getImageValidationRules(), ]); $bookIds = explode(',', $request->get('books', '')); @@ -95,6 +98,7 @@ class BookshelfController extends Controller /** * Display the bookshelf of the given slug. + * * @throws NotFoundException */ public function show(string $slug) @@ -115,13 +119,14 @@ class BookshelfController extends Controller $view = setting()->getForCurrentUser('bookshelf_view_type'); $this->setPageTitle($shelf->getShortName()); + return view('shelves.show', [ - 'shelf' => $shelf, + 'shelf' => $shelf, 'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks, - 'view' => $view, - 'activity' => Activity::entityActivity($shelf, 20, 1), - 'order' => $order, - 'sort' => $sort + 'view' => $view, + 'activity' => Activity::entityActivity($shelf, 20, 1), + 'order' => $order, + 'sort' => $sort, ]); } @@ -137,6 +142,7 @@ class BookshelfController extends Controller $books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get(); $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()])); + return view('shelves.edit', [ 'shelf' => $shelf, 'books' => $books, @@ -145,6 +151,7 @@ class BookshelfController extends Controller /** * Update the specified bookshelf in storage. + * * @throws ValidationException * @throws ImageUploadException * @throws NotFoundException @@ -154,12 +161,11 @@ class BookshelfController extends Controller $shelf = $this->bookshelfRepo->getBySlug($slug); $this->checkOwnablePermission('bookshelf-update', $shelf); $this->validate($request, [ - 'name' => 'required|string|max:255', + 'name' => 'required|string|max:255', 'description' => 'string|max:1000', - 'image' => 'nullable|' . $this->getImageValidationRules(), + 'image' => 'nullable|' . $this->getImageValidationRules(), ]); - $bookIds = explode(',', $request->get('books', '')); $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds); $resetCover = $request->has('image_reset'); @@ -169,7 +175,7 @@ class BookshelfController extends Controller } /** - * Shows the page to confirm deletion + * Shows the page to confirm deletion. */ public function showDelete(string $slug) { @@ -177,11 +183,13 @@ class BookshelfController extends Controller $this->checkOwnablePermission('bookshelf-delete', $shelf); $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()])); + return view('shelves.delete', ['shelf' => $shelf]); } /** * Remove the specified bookshelf from storage. + * * @throws Exception */ public function destroy(string $slug) @@ -218,6 +226,7 @@ class BookshelfController extends Controller $permissionsUpdater->updateFromPermissionsForm($shelf, $request); $this->showSuccessNotification(trans('entities.shelves_permissions_updated')); + return redirect($shelf->getUrl()); } @@ -231,6 +240,7 @@ class BookshelfController extends Controller $updateCount = $this->bookshelfRepo->copyDownPermissions($shelf); $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount])); + return redirect($shelf->getUrl()); } } diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index d65b43cc1..b27fb4f77 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -1,9 +1,11 @@ -checkOwnablePermission('chapter-create', $book); $this->setPageTitle(trans('entities.chapters_create')); + return view('chapters.create', ['book' => $book, 'current' => $book]); } /** * Store a newly created chapter in storage. + * * @throws ValidationException */ public function store(Request $request, string $bookSlug) { $this->validate($request, [ - 'name' => 'required|string|max:255' + 'name' => 'required|string|max:255', ]); $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail(); @@ -69,14 +72,15 @@ class ChapterController extends Controller View::incrementFor($chapter); $this->setPageTitle($chapter->getShortName()); + return view('chapters.show', [ - 'book' => $chapter->book, - 'chapter' => $chapter, - 'current' => $chapter, + 'book' => $chapter->book, + 'chapter' => $chapter, + 'current' => $chapter, 'sidebarTree' => $sidebarTree, - 'pages' => $pages, - 'next' => $nextPreviousLocator->getNext(), - 'previous' => $nextPreviousLocator->getPrevious(), + 'pages' => $pages, + 'next' => $nextPreviousLocator->getNext(), + 'previous' => $nextPreviousLocator->getPrevious(), ]); } @@ -89,11 +93,13 @@ class ChapterController extends Controller $this->checkOwnablePermission('chapter-update', $chapter); $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()])); + return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]); } /** * Update the specified chapter in storage. + * * @throws NotFoundException */ public function update(Request $request, string $bookSlug, string $chapterSlug) @@ -108,6 +114,7 @@ class ChapterController extends Controller /** * Shows the page to confirm deletion of this chapter. + * * @throws NotFoundException */ public function showDelete(string $bookSlug, string $chapterSlug) @@ -116,11 +123,13 @@ class ChapterController extends Controller $this->checkOwnablePermission('chapter-delete', $chapter); $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()])); + return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]); } /** * Remove the specified chapter from storage. + * * @throws NotFoundException * @throws Throwable */ @@ -136,6 +145,7 @@ class ChapterController extends Controller /** * Show the page for moving a chapter. + * * @throws NotFoundException */ public function showMove(string $bookSlug, string $chapterSlug) @@ -147,12 +157,13 @@ class ChapterController extends Controller return view('chapters.move', [ 'chapter' => $chapter, - 'book' => $chapter->book + 'book' => $chapter->book, ]); } /** * Perform the move action for a chapter. + * * @throws NotFoundException */ public function move(Request $request, string $bookSlug, string $chapterSlug) @@ -170,15 +181,18 @@ class ChapterController extends Controller $newBook = $this->chapterRepo->move($chapter, $entitySelection); } catch (MoveOperationException $exception) { $this->showErrorNotification(trans('errors.selected_book_not_found')); + return redirect()->back(); } $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name])); + return redirect($chapter->getUrl()); } /** * Show the Restrictions view. + * * @throws NotFoundException */ public function showPermissions(string $bookSlug, string $chapterSlug) @@ -193,6 +207,7 @@ class ChapterController extends Controller /** * Set the restrictions for this chapter. + * * @throws NotFoundException */ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $chapterSlug) @@ -203,6 +218,7 @@ class ChapterController extends Controller $permissionsUpdater->updateFromPermissionsForm($chapter, $request); $this->showSuccessNotification(trans('entities.chapters_permissions_success')); + return redirect($chapter->getUrl()); } } diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php index 52d087442..480280c99 100644 --- a/app/Http/Controllers/ChapterExportController.php +++ b/app/Http/Controllers/ChapterExportController.php @@ -1,13 +1,14 @@ -chapterRepo = $chapterRepo; $this->exportFormatter = $exportFormatter; + $this->middleware('can:content-export'); } /** * Exports a chapter to pdf. + * * @throws NotFoundException * @throws Throwable */ @@ -29,11 +32,13 @@ class ChapterExportController extends Controller { $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $pdfContent = $this->exportFormatter->chapterToPdf($chapter); + return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf'); } /** * Export a chapter to a self-contained HTML file. + * * @throws NotFoundException * @throws Throwable */ @@ -41,17 +46,34 @@ class ChapterExportController extends Controller { $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter); + return $this->downloadResponse($containedHtml, $chapterSlug . '.html'); } /** * Export a chapter to a simple plaintext .txt file. + * * @throws NotFoundException */ public function plainText(string $bookSlug, string $chapterSlug) { $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); $chapterText = $this->exportFormatter->chapterToPlainText($chapter); + return $this->downloadResponse($chapterText, $chapterSlug . '.txt'); } + + /** + * Export a chapter to a simple markdown file. + * + * @throws NotFoundException + */ + public function markdown(string $bookSlug, string $chapterSlug) + { + // TODO: This should probably export to a zip file. + $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapterText = $this->exportFormatter->chapterToMarkdown($chapter); + + return $this->downloadResponse($chapterText, $chapterSlug . '.md'); + } } diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index bf1a76f51..dfe468f5f 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -1,7 +1,7 @@ -validate($request, [ - 'text' => 'required|string', + 'text' => 'required|string', 'parent_id' => 'nullable|integer', ]); @@ -40,11 +41,13 @@ class CommentController extends Controller // Create a new comment. $this->checkPermission('comment-create-all'); $comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id')); + return view('comments.comment', ['comment' => $comment]); } /** * Update an existing comment. + * * @throws ValidationException */ public function update(Request $request, int $commentId) @@ -58,6 +61,7 @@ class CommentController extends Controller $this->checkOwnablePermission('comment-update', $comment); $comment = $this->commentRepo->update($comment, $request->get('text')); + return view('comments.comment', ['comment' => $comment]); } @@ -70,6 +74,7 @@ class CommentController extends Controller $this->checkOwnablePermission('comment-delete', $comment); $this->commentRepo->delete($comment); + return response()->json(['message' => trans('entities.comment_deleted')]); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 47b03b28d..283a01cfb 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,7 +4,6 @@ namespace BookStack\Http\Controllers; use BookStack\Facades\Activity; use BookStack\Interfaces\Loggable; -use BookStack\HasCreatorAndUpdater; use BookStack\Model; use finfo; use Illuminate\Foundation\Bus\DispatchesJobs; @@ -16,7 +15,8 @@ use Illuminate\Routing\Controller as BaseController; abstract class Controller extends BaseController { - use DispatchesJobs, ValidatesRequests; + use DispatchesJobs; + use ValidatesRequests; /** * Check if the current user is signed in. @@ -106,7 +106,7 @@ abstract class Controller extends BaseController /** * Send back a json error message. */ - protected function jsonError(string $messageText = "", int $statusCode = 500): JsonResponse + protected function jsonError(string $messageText = '', int $statusCode = 500): JsonResponse { return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode); } @@ -118,7 +118,7 @@ abstract class Controller extends BaseController { return response()->make($content, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $fileName . '"' + 'Content-Disposition' => 'attachment; filename="' . $fileName . '"', ]); } @@ -130,9 +130,10 @@ abstract class Controller extends BaseController { $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->buffer($content) ?: 'application/octet-stream'; + return response()->make($content, 200, [ 'Content-Type' => $mime, - 'Content-Disposition' => 'inline; filename="' . $fileName . '"' + 'Content-Disposition' => 'inline; filename="' . $fileName . '"', ]); } @@ -162,6 +163,7 @@ abstract class Controller extends BaseController /** * Log an activity in the system. + * * @param string|Loggable */ protected function logActivity(string $type, $detail = ''): void diff --git a/app/Http/Controllers/FavouriteController.php b/app/Http/Controllers/FavouriteController.php index f4aeb4faa..a990ff825 100644 --- a/app/Http/Controllers/FavouriteController.php +++ b/app/Http/Controllers/FavouriteController.php @@ -17,13 +17,13 @@ class FavouriteController extends Controller { $viewCount = 20; $page = intval($request->get('page', 1)); - $favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount)); + $favourites = (new TopFavourites())->run($viewCount + 1, (($page - 1) * $viewCount)); - $hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null; + $hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null; return view('common.detailed-listing-with-more', [ - 'title' => trans('entities.my_favourites'), - 'entities' => $favourites->slice(0, $viewCount), + 'title' => trans('entities.my_favourites'), + 'entities' => $favourites->slice(0, $viewCount), 'hasMoreLink' => $hasMoreLink, ]); } @@ -41,6 +41,7 @@ class FavouriteController extends Controller $this->showSuccessNotification(trans('activities.favourite_add_notification', [ 'name' => $favouritable->name, ])); + return redirect()->back(); } @@ -57,6 +58,7 @@ class FavouriteController extends Controller $this->showSuccessNotification(trans('activities.favourite_remove_notification', [ 'name' => $favouritable->name, ])); + return redirect()->back(); } @@ -68,7 +70,7 @@ class FavouriteController extends Controller { $modelInfo = $this->validate($request, [ 'type' => 'required|string', - 'id' => 'required|integer', + 'id' => 'required|integer', ]); if (!class_exists($modelInfo['type'])) { @@ -76,8 +78,8 @@ class FavouriteController extends Controller } /** @var Model $model */ - $model = new $modelInfo['type']; - if (! $model instanceof Favouritable) { + $model = new $modelInfo['type'](); + if (!$model instanceof Favouritable) { throw new \Exception('Model not favouritable'); } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 7bc170526..6706de575 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -1,18 +1,18 @@ - 0 ? 0.5 : 1; $recents = $this->isSignedIn() ? - (new RecentlyViewed)->run(12*$recentFactor, 1) + (new RecentlyViewed())->run(12 * $recentFactor, 1) : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get(); - $favourites = (new TopFavourites)->run(6); + $favourites = (new TopFavourites())->run(6); $recentlyUpdatedPages = Page::visible()->with('book') ->where('draft', false) ->orderBy('updated_at', 'desc') ->take($favourites->count() > 0 ? 6 : 12) + ->select(Page::$listAttributes) ->get(); $homepageOptions = ['default', 'books', 'bookshelves', 'page']; @@ -49,11 +50,11 @@ class HomeController extends Controller } $commonData = [ - 'activity' => $activity, - 'recents' => $recents, + 'activity' => $activity, + 'recents' => $recents, 'recentlyUpdatedPages' => $recentlyUpdatedPages, - 'draftPages' => $draftPages, - 'favourites' => $favourites, + 'draftPages' => $draftPages, + 'favourites' => $favourites, ]; // Add required list ordering & sorting for books & shelves views. @@ -64,15 +65,15 @@ class HomeController extends Controller $order = setting()->getForCurrentUser($key . '_sort_order', 'asc'); $sortOptions = [ - 'name' => trans('common.sort_name'), + 'name' => trans('common.sort_name'), 'created_at' => trans('common.sort_created_at'), 'updated_at' => trans('common.sort_updated_at'), ]; $commonData = array_merge($commonData, [ - 'view' => $view, - 'sort' => $sort, - 'order' => $order, + 'view' => $view, + 'sort' => $sort, + 'order' => $order, 'sortOptions' => $sortOptions, ]); } @@ -80,14 +81,16 @@ class HomeController extends Controller if ($homepageOption === 'bookshelves') { $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']); $data = array_merge($commonData, ['shelves' => $shelves]); - return view('common.home-shelves', $data); + + return view('home.shelves', $data); } if ($homepageOption === 'books') { $bookRepo = app(BookRepo::class); $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']); $data = array_merge($commonData, ['books' => $books]); - return view('common.home-book', $data); + + return view('home.books', $data); } if ($homepageOption === 'page') { @@ -96,25 +99,25 @@ class HomeController extends Controller $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id); $pageContent = new PageContent($customHomepage); $customHomepage->html = $pageContent->render(true); - return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage])); + + return view('home.specific-page', array_merge($commonData, ['customHomepage' => $customHomepage])); } - return view('common.home', $commonData); + return view('home.default', $commonData); } /** * Get custom head HTML, Used in ajax calls to show in editor. - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function customHeadContent() { - return view('partials.custom-head'); + return view('common.custom-head'); } /** - * Show the view for /robots.txt + * Show the view for /robots.txt. */ - public function getRobots() + public function robots() { $sitePublic = setting('app-public', false); $allowRobots = config('app.allow_robots'); @@ -124,14 +127,14 @@ class HomeController extends Controller } return response() - ->view('common.robots', ['allowRobots' => $allowRobots]) + ->view('misc.robots', ['allowRobots' => $allowRobots]) ->header('Content-Type', 'text/plain'); } /** * Show the route for 404 responses. */ - public function getNotFound() + public function notFound() { return response()->view('errors.404', [], 404); } diff --git a/app/Http/Controllers/Images/DrawioImageController.php b/app/Http/Controllers/Images/DrawioImageController.php index 462ab68f6..d99bb8e6f 100644 --- a/app/Http/Controllers/Images/DrawioImageController.php +++ b/app/Http/Controllers/Images/DrawioImageController.php @@ -3,10 +3,10 @@ namespace BookStack\Http\Controllers\Images; use BookStack\Exceptions\ImageUploadException; +use BookStack\Http\Controllers\Controller; use BookStack\Uploads\ImageRepo; use Exception; use Illuminate\Http\Request; -use BookStack\Http\Controllers\Controller; class DrawioImageController extends Controller { @@ -29,21 +29,23 @@ class DrawioImageController extends Controller $parentTypeFilter = $request->get('filter_type', null); $imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); - return view('components.image-manager-list', [ - 'images' => $imgData['images'], + + return view('pages.parts.image-manager-list', [ + 'images' => $imgData['images'], 'hasMore' => $imgData['has_more'], ]); } /** * Store a new gallery image in the system. + * * @throws Exception */ public function create(Request $request) { $this->validate($request, [ - 'image' => 'required|string', - 'uploaded_to' => 'required|integer' + 'image' => 'required|string', + 'uploaded_to' => 'required|integer', ]); $this->checkPermission('image-create-all'); @@ -67,16 +69,16 @@ class DrawioImageController extends Controller $image = $this->imageRepo->getById($id); $page = $image->getPage(); if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) { - return $this->jsonError("Image data could not be found"); + return $this->jsonError('Image data could not be found'); } $imageData = $this->imageRepo->getImageData($image); if ($imageData === null) { - return $this->jsonError("Image data could not be found"); + return $this->jsonError('Image data could not be found'); } return response()->json([ - 'content' => base64_encode($imageData) + 'content' => base64_encode($imageData), ]); } } diff --git a/app/Http/Controllers/Images/GalleryImageController.php b/app/Http/Controllers/Images/GalleryImageController.php index c3ad0b7b2..5484411d3 100644 --- a/app/Http/Controllers/Images/GalleryImageController.php +++ b/app/Http/Controllers/Images/GalleryImageController.php @@ -3,9 +3,9 @@ namespace BookStack\Http\Controllers\Images; use BookStack\Exceptions\ImageUploadException; +use BookStack\Http\Controllers\Controller; use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; -use BookStack\Http\Controllers\Controller; use Illuminate\Validation\ValidationException; class GalleryImageController extends Controller @@ -32,21 +32,23 @@ class GalleryImageController extends Controller $parentTypeFilter = $request->get('filter_type', null); $imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); - return view('components.image-manager-list', [ - 'images' => $imgData['images'], + + return view('pages.parts.image-manager-list', [ + 'images' => $imgData['images'], 'hasMore' => $imgData['has_more'], ]); } /** * Store a new gallery image in the system. + * * @throws ValidationException */ public function create(Request $request) { $this->checkPermission('image-create-all'); $this->validate($request, [ - 'file' => $this->getImageValidationRules() + 'file' => $this->getImageValidationRules(), ]); try { diff --git a/app/Http/Controllers/Images/ImageController.php b/app/Http/Controllers/Images/ImageController.php index 1eb8917b3..4070a0e2f 100644 --- a/app/Http/Controllers/Images/ImageController.php +++ b/app/Http/Controllers/Images/ImageController.php @@ -1,4 +1,6 @@ -file($path); } - /** - * Update image details + * Update image details. + * * @throws ImageUploadException * @throws ValidationException */ public function update(Request $request, string $id) { $this->validate($request, [ - 'name' => 'required|min:2|string' + 'name' => 'required|min:2|string', ]); $image = $this->imageRepo->getById($id); @@ -61,14 +64,16 @@ class ImageController extends Controller $image = $this->imageRepo->updateImageDetails($image, $request->all()); $this->imageRepo->loadThumbs($image); - return view('components.image-manager-form', [ - 'image' => $image, + + return view('pages.parts.image-manager-form', [ + 'image' => $image, 'dependantPages' => null, ]); } /** * Get the form for editing the given image. + * * @throws Exception */ public function edit(Request $request, string $id) @@ -81,14 +86,16 @@ class ImageController extends Controller } $this->imageRepo->loadThumbs($image); - return view('components.image-manager-form', [ - 'image' => $image, + + return view('pages.parts.image-manager-form', [ + 'image' => $image, 'dependantPages' => $dependantPages ?? null, ]); } /** - * Deletes an image and all thumbnail/image files + * Deletes an image and all thumbnail/image files. + * * @throws Exception */ public function destroy(string $id) @@ -98,6 +105,7 @@ class ImageController extends Controller $this->checkImagePermission($image); $this->imageRepo->destroyImage($image); + return response(''); } diff --git a/app/Http/Controllers/MaintenanceController.php b/app/Http/Controllers/MaintenanceController.php index 3354a148c..d6abe4682 100644 --- a/app/Http/Controllers/MaintenanceController.php +++ b/app/Http/Controllers/MaintenanceController.php @@ -25,7 +25,7 @@ class MaintenanceController extends Controller $recycleStats = (new TrashCan())->getTrashedCounts(); return view('settings.maintenance', [ - 'version' => $version, + 'version' => $version, 'recycleStats' => $recycleStats, ]); } @@ -45,6 +45,7 @@ class MaintenanceController extends Controller $deleteCount = count($imagesToDelete); if ($deleteCount === 0) { $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found')); + return redirect('/settings/maintenance')->withInput(); } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 31ee4e970..853ac28fc 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -1,12 +1,14 @@ -isSignedIn()) { $draft = $this->pageRepo->getNewDraftPage($parent); + return redirect($draft->getUrl()); } // Otherwise show the edit view if they're a guest $this->setPageTitle(trans('entities.pages_new')); + return view('pages.guest-create', ['parent' => $parent]); } /** * Create a new page as a guest user. + * * @throws ValidationException */ public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null) { $this->validate($request, [ - 'name' => 'required|string|max:255' + 'name' => 'required|string|max:255', ]); $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug); @@ -64,7 +69,7 @@ class PageController extends Controller $page = $this->pageRepo->getNewDraftPage($parent); $this->pageRepo->publishDraft($page, [ 'name' => $request->get('name'), - 'html' => '' + 'html' => '', ]); return redirect($page->getUrl('/edit')); @@ -72,6 +77,7 @@ class PageController extends Controller /** * Show form to continue editing a draft page. + * * @throws NotFoundException */ public function editDraft(string $bookSlug, int $pageId) @@ -84,23 +90,24 @@ class PageController extends Controller $templates = $this->pageRepo->getTemplates(10); return view('pages.edit', [ - 'page' => $draft, - 'book' => $draft->book, - 'isDraft' => true, + 'page' => $draft, + 'book' => $draft->book, + 'isDraft' => true, 'draftsEnabled' => $draftsEnabled, - 'templates' => $templates, + 'templates' => $templates, ]); } /** * Store a new page by changing a draft into a page. + * * @throws NotFoundException * @throws ValidationException */ public function store(Request $request, string $bookSlug, int $pageId) { $this->validate($request, [ - 'name' => 'required|string|max:255' + 'name' => 'required|string|max:255', ]); $draftPage = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-create', $draftPage->getParent()); @@ -113,6 +120,7 @@ class PageController extends Controller /** * Display the specified page. * If the page is not found via the slug the revisions are searched for a match. + * * @throws NotFoundException */ public function show(string $bookSlug, string $pageSlug) @@ -146,20 +154,22 @@ class PageController extends Controller View::incrementFor($page); $this->setPageTitle($page->getShortName()); + return view('pages.show', [ - 'page' => $page, - 'book' => $page->book, - 'current' => $page, - 'sidebarTree' => $sidebarTree, + 'page' => $page, + 'book' => $page->book, + 'current' => $page, + 'sidebarTree' => $sidebarTree, 'commentsEnabled' => $commentsEnabled, - 'pageNav' => $pageNav, - 'next' => $nextPreviousLocator->getNext(), - 'previous' => $nextPreviousLocator->getPrevious(), + 'pageNav' => $pageNav, + 'next' => $nextPreviousLocator->getNext(), + 'previous' => $nextPreviousLocator->getPrevious(), ]); } /** * Get page from an ajax request. + * * @throws NotFoundException */ public function getPageAjax(int $pageId) @@ -167,11 +177,13 @@ class PageController extends Controller $page = $this->pageRepo->getById($pageId); $page->setHidden(array_diff($page->getHidden(), ['html', 'markdown'])); $page->addHidden(['book']); + return response()->json($page); } /** * Show the form for editing the specified page. + * * @throws NotFoundException */ public function edit(string $bookSlug, string $pageSlug) @@ -203,24 +215,26 @@ class PageController extends Controller $templates = $this->pageRepo->getTemplates(10); $draftsEnabled = $this->isSignedIn(); $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()])); + return view('pages.edit', [ - 'page' => $page, - 'book' => $page->book, - 'current' => $page, + 'page' => $page, + 'book' => $page->book, + 'current' => $page, 'draftsEnabled' => $draftsEnabled, - 'templates' => $templates, + 'templates' => $templates, ]); } /** * Update the specified page in storage. + * * @throws ValidationException * @throws NotFoundException */ public function update(Request $request, string $bookSlug, string $pageSlug) { $this->validate($request, [ - 'name' => 'required|string|max:255' + 'name' => 'required|string|max:255', ]); $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-update', $page); @@ -232,6 +246,7 @@ class PageController extends Controller /** * Save a draft update as a revision. + * * @throws NotFoundException */ public function saveDraft(Request $request, int $pageId) @@ -246,25 +261,29 @@ class PageController extends Controller $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown'])); $updateTime = $draft->updated_at->timestamp; + return response()->json([ - 'status' => 'success', - 'message' => trans('entities.pages_edit_draft_save_at'), - 'timestamp' => $updateTime + 'status' => 'success', + 'message' => trans('entities.pages_edit_draft_save_at'), + 'timestamp' => $updateTime, ]); } /** * Redirect from a special link url which uses the page id rather than the name. + * * @throws NotFoundException */ public function redirectFromLink(int $pageId) { $page = $this->pageRepo->getById($pageId); + return redirect($page->getUrl()); } /** * Show the deletion page for the specified page. + * * @throws NotFoundException */ public function showDelete(string $bookSlug, string $pageSlug) @@ -272,15 +291,17 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-delete', $page); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()])); + return view('pages.delete', [ - 'book' => $page->book, - 'page' => $page, - 'current' => $page + 'book' => $page->book, + 'page' => $page, + 'current' => $page, ]); } /** * Show the deletion page for the specified page. + * * @throws NotFoundException */ public function showDeleteDraft(string $bookSlug, int $pageId) @@ -288,15 +309,17 @@ class PageController extends Controller $page = $this->pageRepo->getById($pageId); $this->checkOwnablePermission('page-update', $page); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()])); + return view('pages.delete', [ - 'book' => $page->book, - 'page' => $page, - 'current' => $page + 'book' => $page->book, + 'page' => $page, + 'current' => $page, ]); } /** * Remove the specified page from storage. + * * @throws NotFoundException * @throws Throwable */ @@ -313,6 +336,7 @@ class PageController extends Controller /** * Remove the specified draft page from storage. + * * @throws NotFoundException * @throws Throwable */ @@ -330,6 +354,7 @@ class PageController extends Controller if ($chapter && userCan('view', $chapter)) { return redirect($chapter->getUrl()); } + return redirect($book->getUrl()); } @@ -343,13 +368,14 @@ class PageController extends Controller ->setPath(url('/pages/recently-updated')); return view('common.detailed-listing-paginated', [ - 'title' => trans('entities.recently_updated_pages'), - 'entities' => $pages + 'title' => trans('entities.recently_updated_pages'), + 'entities' => $pages, ]); } /** * Show the view to choose a new parent to move a page into. + * * @throws NotFoundException */ public function showMove(string $bookSlug, string $pageSlug) @@ -357,14 +383,16 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-update', $page); $this->checkOwnablePermission('page-delete', $page); + return view('pages.move', [ 'book' => $page->book, - 'page' => $page + 'page' => $page, ]); } /** * Does the action of moving the location of a page. + * * @throws NotFoundException * @throws Throwable */ @@ -387,15 +415,18 @@ class PageController extends Controller } $this->showErrorNotification(trans('errors.selected_book_chapter_not_found')); + return redirect()->back(); } $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name])); + return redirect($page->getUrl()); } /** * Show the view to copy a page. + * * @throws NotFoundException */ public function showCopy(string $bookSlug, string $pageSlug) @@ -403,15 +434,16 @@ class PageController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('page-view', $page); session()->flashInput(['name' => $page->name]); + return view('pages.copy', [ 'book' => $page->book, - 'page' => $page + 'page' => $page, ]); } - /** * Create a copy of a page within the requested target destination. + * * @throws NotFoundException * @throws Throwable */ @@ -431,21 +463,25 @@ class PageController extends Controller } $this->showErrorNotification(trans('errors.selected_book_chapter_not_found')); + return redirect()->back(); } $this->showSuccessNotification(trans('entities.pages_copy_success')); + return redirect($pageCopy->getUrl()); } /** * Show the Permissions view. + * * @throws NotFoundException */ public function showPermissions(string $bookSlug, string $pageSlug) { $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $this->checkOwnablePermission('restrictions-manage', $page); + return view('pages.permissions', [ 'page' => $page, ]); @@ -453,6 +489,7 @@ class PageController extends Controller /** * Set the permissions for this page. + * * @throws NotFoundException * @throws Throwable */ @@ -464,6 +501,7 @@ class PageController extends Controller $permissionsUpdater->updateFromPermissionsForm($page, $request); $this->showSuccessNotification(trans('entities.pages_permissions_success')); + return redirect($page->getUrl()); } } diff --git a/app/Http/Controllers/PageExportController.php b/app/Http/Controllers/PageExportController.php index e5e027fe7..0287916de 100644 --- a/app/Http/Controllers/PageExportController.php +++ b/app/Http/Controllers/PageExportController.php @@ -2,15 +2,14 @@ namespace BookStack\Http\Controllers; +use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\PageContent; -use BookStack\Entities\Repos\PageRepo; use BookStack\Exceptions\NotFoundException; use Throwable; class PageExportController extends Controller { - protected $pageRepo; protected $exportFormatter; @@ -21,11 +20,13 @@ class PageExportController extends Controller { $this->pageRepo = $pageRepo; $this->exportFormatter = $exportFormatter; + $this->middleware('can:content-export'); } /** * Exports a page to a PDF. - * https://github.com/barryvdh/laravel-dompdf + * https://github.com/barryvdh/laravel-dompdf. + * * @throws NotFoundException * @throws Throwable */ @@ -34,11 +35,13 @@ class PageExportController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page->html = (new PageContent($page))->render(); $pdfContent = $this->exportFormatter->pageToPdf($page); + return $this->downloadResponse($pdfContent, $pageSlug . '.pdf'); } /** * Export a page to a self-contained HTML file. + * * @throws NotFoundException * @throws Throwable */ @@ -47,17 +50,33 @@ class PageExportController extends Controller $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $page->html = (new PageContent($page))->render(); $containedHtml = $this->exportFormatter->pageToContainedHtml($page); + return $this->downloadResponse($containedHtml, $pageSlug . '.html'); } /** * Export a page to a simple plaintext .txt file. + * * @throws NotFoundException */ public function plainText(string $bookSlug, string $pageSlug) { $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); $pageText = $this->exportFormatter->pageToPlainText($page); + return $this->downloadResponse($pageText, $pageSlug . '.txt'); } + + /** + * Export a page to a simple markdown .md file. + * + * @throws NotFoundException + */ + public function markdown(string $bookSlug, string $pageSlug) + { + $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); + $pageText = $this->exportFormatter->pageToMarkdown($page); + + return $this->downloadResponse($pageText, $pageSlug . '.md'); + } } diff --git a/app/Http/Controllers/PageRevisionController.php b/app/Http/Controllers/PageRevisionController.php index 4c4333016..d595a6e26 100644 --- a/app/Http/Controllers/PageRevisionController.php +++ b/app/Http/Controllers/PageRevisionController.php @@ -1,13 +1,14 @@ -pageRepo->getBySlug($bookSlug, $pageSlug); $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()])); + return view('pages.revisions', [ - 'page' => $page, - 'current' => $page + 'page' => $page, + 'current' => $page, ]); } /** * Shows a preview of a single revision. + * * @throws NotFoundException */ public function show(string $bookSlug, string $pageSlug, int $revisionId) @@ -50,16 +54,18 @@ class PageRevisionController extends Controller $page->html = (new PageContent($page))->render(); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()])); + return view('pages.revision', [ - 'page' => $page, - 'book' => $page->book, - 'diff' => null, - 'revision' => $revision + 'page' => $page, + 'book' => $page->book, + 'diff' => null, + 'revision' => $revision, ]); } /** * Shows the changes of a single revision. + * * @throws NotFoundException */ public function changes(string $bookSlug, string $pageSlug, int $revisionId) @@ -81,15 +87,16 @@ class PageRevisionController extends Controller $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()])); return view('pages.revision', [ - 'page' => $page, - 'book' => $page->book, - 'diff' => $diff, - 'revision' => $revision + 'page' => $page, + 'book' => $page->book, + 'diff' => $diff, + 'revision' => $revision, ]); } /** * Restores a page using the content of the specified revision. + * * @throws NotFoundException */ public function restore(string $bookSlug, string $pageSlug, int $revisionId) @@ -104,6 +111,7 @@ class PageRevisionController extends Controller /** * Deletes a revision using the id of the specified revision. + * * @throws NotFoundException */ public function destroy(string $bookSlug, string $pageSlug, int $revId) @@ -122,11 +130,13 @@ class PageRevisionController extends Controller // Check if its the latest revision, cannot delete latest revision. if (intval($currentRevision->id) === intval($revId)) { $this->showErrorNotification(trans('entities.revision_cannot_delete_latest')); + return redirect($page->getUrl('/revisions')); } $revision->delete(); $this->showSuccessNotification(trans('entities.revision_delete_success')); + return redirect($page->getUrl('/revisions')); } } diff --git a/app/Http/Controllers/PageTemplateController.php b/app/Http/Controllers/PageTemplateController.php index 2307bc0d5..1e24c29ee 100644 --- a/app/Http/Controllers/PageTemplateController.php +++ b/app/Http/Controllers/PageTemplateController.php @@ -11,7 +11,7 @@ class PageTemplateController extends Controller protected $pageRepo; /** - * PageTemplateController constructor + * PageTemplateController constructor. */ public function __construct(PageRepo $pageRepo) { @@ -31,13 +31,14 @@ class PageTemplateController extends Controller $templates->appends(['search' => $search]); } - return view('pages.template-manager-list', [ - 'templates' => $templates + return view('pages.parts.template-manager-list', [ + 'templates' => $templates, ]); } /** * Get the content of a template. + * * @throws NotFoundException */ public function get(int $templateId) @@ -49,7 +50,7 @@ class PageTemplateController extends Controller } return response()->json([ - 'html' => $page->html, + 'html' => $page->html, 'markdown' => $page->markdown, ]); } diff --git a/app/Http/Controllers/RecycleBinController.php b/app/Http/Controllers/RecycleBinController.php index a644a2889..1736023a5 100644 --- a/app/Http/Controllers/RecycleBinController.php +++ b/app/Http/Controllers/RecycleBinController.php @@ -1,12 +1,14 @@ -middleware(function ($request, $next) { $this->checkPermission('settings-manage'); $this->checkPermission('restrictions-manage-all'); + return $next($request); }); } - /** * Show the top-level listing for the recycle bin. */ @@ -31,6 +33,7 @@ class RecycleBinController extends Controller $deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10); $this->setPageTitle(trans('settings.recycle_bin')); + return view('settings.recycle-bin.index', [ 'deletions' => $deletions, ]); @@ -44,13 +47,29 @@ class RecycleBinController extends Controller /** @var Deletion $deletion */ $deletion = Deletion::query()->findOrFail($id); + // Walk the parent chain to find any cascading parent deletions + $currentDeletable = $deletion->deletable; + $searching = true; + while ($searching && $currentDeletable instanceof Entity) { + $parent = $currentDeletable->getParent(); + if ($parent && $parent->trashed()) { + $currentDeletable = $parent; + } else { + $searching = false; + } + } + /** @var ?Deletion $parentDeletion */ + $parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first(); + return view('settings.recycle-bin.restore', [ - 'deletion' => $deletion, + 'deletion' => $deletion, + 'parentDeletion' => $parentDeletion, ]); } /** * Restore the element attached to the given deletion. + * * @throws \Exception */ public function restore(string $id) @@ -61,6 +80,7 @@ class RecycleBinController extends Controller $restoreCount = (new TrashCan())->restoreFromDeletion($deletion); $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount])); + return redirect($this->recycleBinBaseUrl); } @@ -79,6 +99,7 @@ class RecycleBinController extends Controller /** * Permanently delete the content associated with the given deletion. + * * @throws \Exception */ public function destroy(string $id) @@ -89,11 +110,13 @@ class RecycleBinController extends Controller $deleteCount = (new TrashCan())->destroyFromDeletion($deletion); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); + return redirect($this->recycleBinBaseUrl); } /** * Empty out the recycle bin. + * * @throws \Exception */ public function empty() @@ -102,6 +125,7 @@ class RecycleBinController extends Controller $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY); $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount])); + return redirect($this->recycleBinBaseUrl); } } diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php index e16a724a4..06a30e99d 100644 --- a/app/Http/Controllers/RoleController.php +++ b/app/Http/Controllers/RoleController.php @@ -1,4 +1,6 @@ -checkPermission('user-roles-manage'); $roles = $this->permissionsRepo->getAllRoles(); + return view('settings.roles.index', ['roles' => $roles]); } /** - * Show the form to create a new role + * Show the form to create a new role. */ public function create() { $this->checkPermission('user-roles-manage'); + return view('settings.roles.create'); } @@ -46,16 +49,18 @@ class RoleController extends Controller $this->checkPermission('user-roles-manage'); $this->validate($request, [ 'display_name' => 'required|min:3|max:180', - 'description' => 'max:180' + 'description' => 'max:180', ]); $this->permissionsRepo->saveNewRole($request->all()); $this->showSuccessNotification(trans('settings.role_create_success')); + return redirect('/settings/roles'); } /** * Show the form for editing a user role. + * * @throws PermissionsException */ public function edit(string $id) @@ -65,11 +70,13 @@ class RoleController extends Controller if ($role->hidden) { throw new PermissionsException(trans('errors.role_cannot_be_edited')); } + return view('settings.roles.edit', ['role' => $role]); } /** * Updates a user role. + * * @throws ValidationException */ public function update(Request $request, string $id) @@ -77,11 +84,12 @@ class RoleController extends Controller $this->checkPermission('user-roles-manage'); $this->validate($request, [ 'display_name' => 'required|min:3|max:180', - 'description' => 'max:180' + 'description' => 'max:180', ]); $this->permissionsRepo->updateRole($id, $request->all()); $this->showSuccessNotification(trans('settings.role_update_success')); + return redirect('/settings/roles'); } @@ -96,12 +104,14 @@ class RoleController extends Controller $roles = $this->permissionsRepo->getAllRolesExcept($role); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); $roles->prepend($blankRole); + return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]); } /** * Delete a role from the system, * Migrate from a previous role if set. + * * @throws Exception */ public function delete(Request $request, string $id) @@ -112,10 +122,12 @@ class RoleController extends Controller $this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id')); } catch (PermissionsException $e) { $this->showErrorNotification($e->getMessage()); + return redirect()->back(); } $this->showSuccessNotification(trans('settings.role_delete_success')); + return redirect('/settings/roles'); } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 859857500..d12c23b5a 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -1,9 +1,11 @@ -setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString])); $page = intval($request->get('page', '0')) ?: 1; - $nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page+1)); + $nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page + 1)); $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20); return view('search.all', [ - 'entities' => $results['results'], + 'entities' => $results['results'], 'totalResults' => $results['total'], - 'searchTerm' => $fullSearchString, - 'hasNextPage' => $results['has_more'], + 'searchTerm' => $fullSearchString, + 'hasNextPage' => $results['has_more'], 'nextPageLink' => $nextPageLink, - 'options' => $searchOpts, + 'options' => $searchOpts, ]); } @@ -51,7 +53,8 @@ class SearchController extends Controller { $term = $request->get('term', ''); $results = $this->searchRunner->searchBook($bookId, $term); - return view('partials.entity-list', ['entities' => $results]); + + return view('entities.list', ['entities' => $results]); } /** @@ -61,7 +64,8 @@ class SearchController extends Controller { $term = $request->get('term', ''); $results = $this->searchRunner->searchChapter($chapterId, $term); - return view('partials.entity-list', ['entities' => $results]); + + return view('entities.list', ['entities' => $results]); } /** @@ -71,18 +75,18 @@ class SearchController extends Controller public function searchEntitiesAjax(Request $request) { $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book']; - $searchTerm = $request->get('term', false); + $searchTerm = $request->get('term', false); $permission = $request->get('permission', 'view'); // Search for entities otherwise show most popular if ($searchTerm !== false) { - $searchTerm .= ' {type:'. implode('|', $entityTypes) .'}'; + $searchTerm .= ' {type:' . implode('|', $entityTypes) . '}'; $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results']; } else { - $entities = (new Popular)->run(20, 0, $entityTypes, $permission); + $entities = (new Popular())->run(20, 0, $entityTypes, $permission); } - return view('search.entity-ajax-list', ['entities' => $entities]); + return view('search.parts.entity-ajax-list', ['entities' => $entities]); } /** @@ -93,7 +97,8 @@ class SearchController extends Controller $type = $request->get('entity_type', null); $id = $request->get('entity_id', null); - $entities = (new SiblingFetcher)->fetch($type, $id); - return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']); + $entities = (new SiblingFetcher())->fetch($type, $id); + + return view('entities.list-basic', ['entities' => $entities, 'style' => 'compact']); } } diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index f02f541bc..d9f172081 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,4 +1,6 @@ - $version, - 'guestUser' => User::getDefault() + 'version' => $version, + 'guestUser' => User::getDefault(), ]); } @@ -72,6 +74,7 @@ class SettingController extends Controller $this->logActivity(ActivityType::SETTINGS_UPDATE, $section); $this->showSuccessNotification(trans('settings.settings_save_success')); $redirectLocation = '/settings#' . $section; + return redirect(rtrim($redirectLocation, '#')); } } diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php index 9f4ed4d89..336e063ab 100644 --- a/app/Http/Controllers/StatusController.php +++ b/app/Http/Controllers/StatusController.php @@ -1,4 +1,6 @@ - $this->trueWithoutError(function () { $rand = Str::random(); Cache::set('status_test', $rand); + return Cache::get('status_test') === $rand; }), 'session' => $this->trueWithoutError(function () { $rand = Str::random(); Session::put('status_test', $rand); + return Session::get('status_test') === $rand; }), ]; $hasError = in_array(false, $statuses); + return response()->json($statuses, $hasError ? 500 : 200); } diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index ce84bf410..b0065af70 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -1,11 +1,12 @@ -get('search', null); $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); + return response()->json($suggestions); } @@ -34,6 +36,7 @@ class TagController extends Controller $searchTerm = $request->get('search', null); $tagName = $request->get('name', null); $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); + return response()->json($suggestions); } } diff --git a/app/Http/Controllers/UserApiTokenController.php b/app/Http/Controllers/UserApiTokenController.php index 3949722ea..bdc25f79d 100644 --- a/app/Http/Controllers/UserApiTokenController.php +++ b/app/Http/Controllers/UserApiTokenController.php @@ -1,4 +1,6 @@ -checkPermissionOrCurrentUser('users-manage', $userId); $user = User::query()->findOrFail($userId); + return view('users.api-tokens.create', [ 'user' => $user, ]); @@ -34,7 +36,7 @@ class UserApiTokenController extends Controller $this->checkPermissionOrCurrentUser('users-manage', $userId); $this->validate($request, [ - 'name' => 'required|max:250', + 'name' => 'required|max:250', 'expires_at' => 'date_format:Y-m-d', ]); @@ -42,10 +44,10 @@ class UserApiTokenController extends Controller $secret = Str::random(32); $token = (new ApiToken())->forceFill([ - 'name' => $request->get('name'), - 'token_id' => Str::random(32), - 'secret' => Hash::make($secret), - 'user_id' => $user->id, + 'name' => $request->get('name'), + 'token_id' => Str::random(32), + 'secret' => Hash::make($secret), + 'user_id' => $user->id, 'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(), ]); @@ -71,9 +73,9 @@ class UserApiTokenController extends Controller $secret = session()->pull('api-token-secret:' . $token->id, null); return view('users.api-tokens.edit', [ - 'user' => $user, - 'token' => $token, - 'model' => $token, + 'user' => $user, + 'token' => $token, + 'model' => $token, 'secret' => $secret, ]); } @@ -84,18 +86,19 @@ class UserApiTokenController extends Controller public function update(Request $request, int $userId, int $tokenId) { $this->validate($request, [ - 'name' => 'required|max:250', + 'name' => 'required|max:250', 'expires_at' => 'date_format:Y-m-d', ]); [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId); $token->fill([ - 'name' => $request->get('name'), + 'name' => $request->get('name'), 'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(), ])->save(); $this->showSuccessNotification(trans('settings.user_api_token_update_success')); $this->logActivity(ActivityType::API_TOKEN_UPDATE, $token); + return redirect($user->getEditUrl('/api-tokens/' . $token->id)); } @@ -105,8 +108,9 @@ class UserApiTokenController extends Controller public function delete(int $userId, int $tokenId) { [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId); + return view('users.api-tokens.delete', [ - 'user' => $user, + 'user' => $user, 'token' => $token, ]); } @@ -138,6 +142,7 @@ class UserApiTokenController extends Controller $user = User::query()->findOrFail($userId); $token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail(); + return [$user, $token]; } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index ba3590437..a0da220ee 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -1,4 +1,6 @@ -checkPermission('users-manage'); $listDetails = [ - 'order' => $request->get('order', 'asc'), + 'order' => $request->get('order', 'asc'), 'search' => $request->get('search', ''), - 'sort' => $request->get('sort', 'name'), + 'sort' => $request->get('sort', 'name'), ]; $users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails); $this->setPageTitle(trans('settings.users')); $users->appends($listDetails); + return view('users.index', ['users' => $users, 'listDetails' => $listDetails]); } @@ -58,11 +60,13 @@ class UserController extends Controller $this->checkPermission('users-manage'); $authMethod = config('auth.method'); $roles = $this->userRepo->getAllRoles(); + return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]); } /** * Store a newly created user in storage. + * * @throws UserUpdateException * @throws ValidationException */ @@ -71,7 +75,7 @@ class UserController extends Controller $this->checkPermission('users-manage'); $validationRules = [ 'name' => 'required', - 'email' => 'required|email|unique:users,email' + 'email' => 'required|email|unique:users,email', ]; $authMethod = config('auth.method'); @@ -108,6 +112,7 @@ class UserController extends Controller $this->userRepo->downloadAndAssignUserAvatar($user); $this->logActivity(ActivityType::USER_CREATE, $user); + return redirect('/settings/users'); } @@ -118,23 +123,28 @@ class UserController extends Controller { $this->checkPermissionOrCurrentUser('users-manage', $id); - $user = $this->user->newQuery()->with(['apiTokens'])->findOrFail($id); + /** @var User $user */ + $user = $this->user->newQuery()->with(['apiTokens', 'mfaValues'])->findOrFail($id); $authMethod = ($user->system_name) ? 'system' : config('auth.method'); $activeSocialDrivers = $socialAuthService->getActiveDrivers(); + $mfaMethods = $user->mfaValues->groupBy('method'); $this->setPageTitle(trans('settings.user_profile')); $roles = $this->userRepo->getAllRoles(); + return view('users.edit', [ - 'user' => $user, + 'user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, - 'authMethod' => $authMethod, - 'roles' => $roles + 'mfaMethods' => $mfaMethods, + 'authMethod' => $authMethod, + 'roles' => $roles, ]); } /** * Update the specified user in storage. + * * @throws UserUpdateException * @throws ImageUploadException * @throws ValidationException @@ -208,6 +218,7 @@ class UserController extends Controller $this->logActivity(ActivityType::USER_UPDATE, $user); $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id); + return redirect($redirectUrl); } @@ -220,11 +231,13 @@ class UserController extends Controller $user = $this->userRepo->getById($id); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); + return view('users.delete', ['user' => $user]); } /** * Remove the specified user from storage. + * * @throws Exception */ public function destroy(Request $request, int $id) @@ -237,11 +250,13 @@ class UserController extends Controller if ($this->userRepo->isOnlyAdmin($user)) { $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin')); + return redirect($user->getEditUrl()); } if ($user->system_name === 'public') { $this->showErrorNotification(trans('errors.users_cannot_delete_guest')); + return redirect($user->getEditUrl()); } @@ -304,6 +319,7 @@ class UserController extends Controller if (!in_array($type, $validSortTypes)) { return redirect()->back(500); } + return $this->changeListSort($id, $request, $type); } @@ -314,6 +330,7 @@ class UserController extends Controller { $enabled = setting()->getForCurrentUser('dark-mode-enabled', false); setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true'); + return redirect()->back(); } @@ -325,14 +342,15 @@ class UserController extends Controller $this->checkPermissionOrCurrentUser('users-manage', $id); $keyWhitelist = ['home-details']; if (!in_array($key, $keyWhitelist)) { - return response("Invalid key", 500); + return response('Invalid key', 500); } $newState = $request->get('expand', 'false'); $user = $this->user->findOrFail($id); setting()->putUser($user, 'section_expansion#' . $key, $newState); - return response("", 204); + + return response('', 204); } /** diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 95e68cb07..09ae4c1bd 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -1,11 +1,13 @@ -getAssetCounts($user); return view('users.profile', [ - 'user' => $user, - 'activity' => $userActivity, + 'user' => $user, + 'activity' => $userActivity, 'recentlyCreated' => $recentlyCreated, - 'assetCounts' => $assetCounts + 'assetCounts' => $assetCounts, ]); } } diff --git a/app/Http/Controllers/UserSearchController.php b/app/Http/Controllers/UserSearchController.php index a0dfbd8d0..f7ed9e57a 100644 --- a/app/Http/Controllers/UserSearchController.php +++ b/app/Http/Controllers/UserSearchController.php @@ -26,6 +26,7 @@ class UserSearchController extends Controller } $users = $query->get(); - return view('components.user-select-list', compact('users')); + + return view('form.user-select-list', compact('users')); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 694036ab4..4b8cdfba4 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -1,4 +1,6 @@ - \BookStack\Http\Middleware\Authenticate::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'can' => \BookStack\Http\Middleware\CheckUserHasPermission::class, 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class, 'guard' => \BookStack\Http\Middleware\CheckGuard::class, + 'mfa-setup' => \BookStack\Http\Middleware\AuthenticatedOrPendingMfa::class, ]; } diff --git a/app/Http/Middleware/ApiAuthenticate.php b/app/Http/Middleware/ApiAuthenticate.php index 728057bed..bc584d3c5 100644 --- a/app/Http/Middleware/ApiAuthenticate.php +++ b/app/Http/Middleware/ApiAuthenticate.php @@ -9,8 +9,6 @@ use Illuminate\Http\Request; class ApiAuthenticate { - use ChecksForEmailConfirmation; - /** * Handle an incoming request. */ @@ -29,6 +27,7 @@ class ApiAuthenticate /** * Ensure the current user can access authenticated API routes, either via existing session * authentication or via API Token authentication. + * * @throws UnauthorizedException */ protected function ensureAuthorizedBySessionOrToken(): void @@ -36,10 +35,10 @@ class ApiAuthenticate // Return if the user is already found to be signed in via session-based auth. // This is to make it easy to browser the API via browser after just logging into the system. if (signedInUser() || session()->isStarted()) { - $this->ensureEmailConfirmedIfRequested(); if (!user()->can('access-api')) { throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403); } + return; } @@ -48,7 +47,6 @@ class ApiAuthenticate // Validate the token and it's users API access auth()->authenticate(); - $this->ensureEmailConfirmedIfRequested(); } /** @@ -58,9 +56,9 @@ class ApiAuthenticate { return response()->json([ 'error' => [ - 'code' => $code, + 'code' => $code, 'message' => $message, - ] + ], ], $code); } } diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index df8c44d35..a32029112 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -7,47 +7,19 @@ use Illuminate\Http\Request; class Authenticate { - use ChecksForEmailConfirmation; - /** * Handle an incoming request. */ public function handle(Request $request, Closure $next) { - if ($this->awaitingEmailConfirmation()) { - return $this->emailConfirmationErrorResponse($request); - } - if (!hasAppAccess()) { if ($request->ajax()) { return response('Unauthorized.', 401); - } else { - return redirect()->guest(url('/login')); } + + return redirect()->guest(url('/login')); } return $next($request); } - - /** - * Provide an error response for when the current user's email is not confirmed - * in a system which requires it. - */ - protected function emailConfirmationErrorResponse(Request $request) - { - if ($request->wantsJson()) { - return response()->json([ - 'error' => [ - 'code' => 401, - 'message' => trans('errors.email_confirmation_awaiting') - ] - ], 401); - } - - if (session()->get('sent-email-confirmation') === true) { - return redirect('/register/confirm'); - } - - return redirect('/register/confirm/awaiting'); - } } diff --git a/app/Http/Middleware/AuthenticatedOrPendingMfa.php b/app/Http/Middleware/AuthenticatedOrPendingMfa.php new file mode 100644 index 000000000..0a0588864 --- /dev/null +++ b/app/Http/Middleware/AuthenticatedOrPendingMfa.php @@ -0,0 +1,40 @@ +loginService = $loginService; + $this->mfaSession = $mfaSession; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @return mixed + */ + public function handle($request, Closure $next) + { + $user = auth()->user(); + $loggedIn = $user !== null; + $lastAttemptUser = $this->loginService->getLastLoginAttemptUser(); + + if ($loggedIn || ($lastAttemptUser && $this->mfaSession->isPendingMfaSetup($lastAttemptUser))) { + return $next($request); + } + + return redirect()->to(url('/login')); + } +} diff --git a/app/Http/Middleware/CheckEmailConfirmed.php b/app/Http/Middleware/CheckEmailConfirmed.php new file mode 100644 index 000000000..7dd970ade --- /dev/null +++ b/app/Http/Middleware/CheckEmailConfirmed.php @@ -0,0 +1,49 @@ +confirmationService = $confirmationService; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @return mixed + */ + public function handle($request, Closure $next) + { + /** @var User $user */ + $user = auth()->user(); + if (auth()->check() && !$user->email_confirmed && $this->confirmationService->confirmationRequired()) { + auth()->logout(); + + return redirect()->to('/'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/CheckGuard.php b/app/Http/Middleware/CheckGuard.php index cc73ea68d..adc1d1f3e 100644 --- a/app/Http/Middleware/CheckGuard.php +++ b/app/Http/Middleware/CheckGuard.php @@ -9,9 +9,10 @@ class CheckGuard /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @param string $allowedGuards + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string $allowedGuards + * * @return mixed */ public function handle($request, Closure $next, ...$allowedGuards) @@ -19,6 +20,7 @@ class CheckGuard $activeGuard = config('auth.method'); if (!in_array($activeGuard, $allowedGuards)) { session()->flash('error', trans('errors.permission')); + return redirect('/'); } diff --git a/app/Http/Middleware/CheckUserHasPermission.php b/app/Http/Middleware/CheckUserHasPermission.php new file mode 100644 index 000000000..4a6a06468 --- /dev/null +++ b/app/Http/Middleware/CheckUserHasPermission.php @@ -0,0 +1,38 @@ +can($permission)) { + return $this->errorResponse($request); + } + + return $next($request); + } + + protected function errorResponse(Request $request) + { + if ($request->wantsJson()) { + return response()->json(['error' => trans('errors.permissionJson')], 403); + } + + session()->flash('error', trans('errors.permission')); + + return redirect('/'); + } +} diff --git a/app/Http/Middleware/ChecksForEmailConfirmation.php b/app/Http/Middleware/ChecksForEmailConfirmation.php deleted file mode 100644 index cbf55040a..000000000 --- a/app/Http/Middleware/ChecksForEmailConfirmation.php +++ /dev/null @@ -1,36 +0,0 @@ -awaitingEmailConfirmation()) { - throw new UnauthorizedException(trans('errors.email_confirmation_awaiting')); - } - } - - /** - * Check if email confirmation is required and the current user is awaiting confirmation. - */ - protected function awaitingEmailConfirmation(): bool - { - if (auth()->check()) { - $requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict')); - if ($requireConfirmation && !auth()->user()->email_confirmed) { - return true; - } - } - - return false; - } -} diff --git a/app/Http/Middleware/ControlIframeSecurity.php b/app/Http/Middleware/ControlIframeSecurity.php index cc8034413..11d9e6d4c 100644 --- a/app/Http/Middleware/ControlIframeSecurity.php +++ b/app/Http/Middleware/ControlIframeSecurity.php @@ -3,7 +3,6 @@ namespace BookStack\Http\Middleware; use Closure; -use Symfony\Component\HttpFoundation\Response; /** * Sets CSP headers to restrict the hosts that BookStack can be @@ -15,8 +14,9 @@ class ControlIframeSecurity /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * * @return mixed */ public function handle($request, Closure $next) @@ -31,6 +31,7 @@ class ControlIframeSecurity $response = $next($request); $cspValue = 'frame-ancestors ' . $iframeHosts->join(' '); $response->headers->set('Content-Security-Policy', $cspValue); + return $response; } } diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php index 0b286a713..e82465146 100644 --- a/app/Http/Middleware/Localization.php +++ b/app/Http/Middleware/Localization.php @@ -1,4 +1,6 @@ - 'ar', - 'bg' => 'bg_BG', - 'bs' => 'bs_BA', - 'ca' => 'ca', - 'da' => 'da_DK', - 'de' => 'de_DE', + 'ar' => 'ar', + 'bg' => 'bg_BG', + 'bs' => 'bs_BA', + 'ca' => 'ca', + 'da' => 'da_DK', + 'de' => 'de_DE', 'de_informal' => 'de_DE', - 'en' => 'en_GB', - 'es' => 'es_ES', - 'es_AR' => 'es_AR', - 'fr' => 'fr_FR', - 'he' => 'he_IL', - 'hr' => 'hr_HR', - 'id' => 'id_ID', - 'it' => 'it_IT', - 'ja' => 'ja', - 'ko' => 'ko_KR', - 'lv' => 'lv_LV', - 'nl' => 'nl_NL', - 'nb' => 'nb_NO', - 'pl' => 'pl_PL', - 'pt' => 'pt_PT', - 'pt_BR' => 'pt_BR', - 'ru' => 'ru', - 'sk' => 'sk_SK', - 'sl' => 'sl_SI', - 'sv' => 'sv_SE', - 'uk' => 'uk_UA', - 'vi' => 'vi_VN', - 'zh_CN' => 'zh_CN', - 'zh_TW' => 'zh_TW', - 'tr' => 'tr_TR', + 'en' => 'en_GB', + 'es' => 'es_ES', + 'es_AR' => 'es_AR', + 'fr' => 'fr_FR', + 'he' => 'he_IL', + 'hr' => 'hr_HR', + 'id' => 'id_ID', + 'it' => 'it_IT', + 'ja' => 'ja', + 'ko' => 'ko_KR', + 'lt' => 'lt_LT', + 'lv' => 'lv_LV', + 'nl' => 'nl_NL', + 'nb' => 'nb_NO', + 'pl' => 'pl_PL', + 'pt' => 'pt_PT', + 'pt_BR' => 'pt_BR', + 'ru' => 'ru', + 'sk' => 'sk_SK', + 'sl' => 'sl_SI', + 'sv' => 'sv_SE', + 'uk' => 'uk_UA', + 'vi' => 'vi_VN', + 'zh_CN' => 'zh_CN', + 'zh_TW' => 'zh_TW', + 'tr' => 'tr_TR', ]; /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * * @return mixed */ public function handle($request, Closure $next) @@ -73,6 +76,7 @@ class Localization app()->setLocale($locale); Carbon::setLocale($locale); $this->setSystemDateLocale($locale); + return $next($request); } @@ -106,11 +110,12 @@ class Localization return $lang; } } + return $default; } /** - * Get the ISO version of a BookStack language name + * Get the ISO version of a BookStack language name. */ public function getLocaleIso(string $locale): string { diff --git a/app/Http/Middleware/PermissionMiddleware.php b/app/Http/Middleware/PermissionMiddleware.php deleted file mode 100644 index d0bb4f79e..000000000 --- a/app/Http/Middleware/PermissionMiddleware.php +++ /dev/null @@ -1,28 +0,0 @@ -user() || !$request->user()->can($permission)) { - session()->flash('error', trans('errors.permission')); - return redirect()->back(); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index c27df7af4..6853809ea 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -1,4 +1,6 @@ -getScheme().'://'.$this->getHttpHost(); + $base = $this->getScheme() . '://' . $this->getHttpHost(); } return $base; diff --git a/app/Interfaces/Favouritable.php b/app/Interfaces/Favouritable.php index dd335feed..8a311d1f2 100644 --- a/app/Interfaces/Favouritable.php +++ b/app/Interfaces/Favouritable.php @@ -1,4 +1,6 @@ - setting('app-name')]; + return $this->newMailMessage() ->subject(trans('auth.email_confirm_subject', $appName)) ->greeting(trans('auth.email_confirm_greeting', $appName)) diff --git a/app/Notifications/MailNotification.php b/app/Notifications/MailNotification.php index 5aa9b1e4a..12159b278 100644 --- a/app/Notifications/MailNotification.php +++ b/app/Notifications/MailNotification.php @@ -1,4 +1,6 @@ -view([ + return (new MailMessage())->view([ 'html' => 'vendor.notifications.email', - 'text' => 'vendor.notifications.email-plain' + 'text' => 'vendor.notifications.email-plain', ]); } } diff --git a/app/Notifications/ResetPassword.php b/app/Notifications/ResetPassword.php index 208752764..7fa146596 100644 --- a/app/Notifications/ResetPassword.php +++ b/app/Notifications/ResetPassword.php @@ -1,4 +1,6 @@ -newMailMessage() + return $this->newMailMessage() ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')])) ->line(trans('auth.email_reset_text')) ->action(trans('auth.reset_password'), url('password/reset/' . $this->token)) diff --git a/app/Notifications/TestEmail.php b/app/Notifications/TestEmail.php index 7fce1c19c..7f59ff70f 100644 --- a/app/Notifications/TestEmail.php +++ b/app/Notifications/TestEmail.php @@ -1,11 +1,14 @@ - setting('app-name')]; + return $this->newMailMessage() ->subject(trans('auth.user_invite_email_subject', $appName)) ->greeting(trans('auth.user_invite_email_greeting', $appName)) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 333542c31..145a7645b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -1,10 +1,13 @@ - Bookshelf::class, - 'BookStack\\Book' => Book::class, - 'BookStack\\Chapter' => Chapter::class, - 'BookStack\\Page' => Page::class, + 'BookStack\\Book' => Book::class, + 'BookStack\\Chapter' => Chapter::class, + 'BookStack\\Page' => Page::class, ]); // View Composers - View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class); + View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class); } /** @@ -65,8 +68,8 @@ class AppServiceProvider extends ServiceProvider return new SettingService($app->make(Setting::class), $app->make(Repository::class)); }); - $this->app->singleton(SocialAuthService::class, function($app) { - return new SocialAuthService($app->make(SocialiteFactory::class)); + $this->app->singleton(SocialAuthService::class, function ($app) { + return new SocialAuthService($app->make(SocialiteFactory::class), $app->make(LoginService::class)); }); } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index fe52df168..71b7ab016 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -8,8 +8,8 @@ use BookStack\Auth\Access\ExternalBaseUserProvider; use BookStack\Auth\Access\Guards\LdapSessionGuard; use BookStack\Auth\Access\Guards\Saml2SessionGuard; use BookStack\Auth\Access\LdapService; +use BookStack\Auth\Access\LoginService; use BookStack\Auth\Access\RegistrationService; -use BookStack\Auth\UserRepo; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider @@ -22,15 +22,16 @@ class AuthServiceProvider extends ServiceProvider public function boot() { Auth::extend('api-token', function ($app, $name, array $config) { - return new ApiTokenGuard($app['request']); + return new ApiTokenGuard($app['request'], $app->make(LoginService::class)); }); Auth::extend('ldap-session', function ($app, $name, array $config) { $provider = Auth::createUserProvider($config['provider']); + return new LdapSessionGuard( $name, $provider, - $this->app['session.store'], + $app['session.store'], $app[LdapService::class], $app[RegistrationService::class] ); @@ -38,10 +39,11 @@ class AuthServiceProvider extends ServiceProvider Auth::extend('saml2-session', function ($app, $name, array $config) { $provider = Auth::createUserProvider($config['provider']); + return new Saml2SessionGuard( $name, $provider, - $this->app['session.store'], + $app['session.store'], $app[RegistrationService::class] ); }); diff --git a/app/Providers/CustomValidationServiceProvider.php b/app/Providers/CustomValidationServiceProvider.php index b668a4cd2..c54f48ca3 100644 --- a/app/Providers/CustomValidationServiceProvider.php +++ b/app/Providers/CustomValidationServiceProvider.php @@ -7,7 +7,6 @@ use Illuminate\Support\ServiceProvider; class CustomValidationServiceProvider extends ServiceProvider { - /** * Register our custom validation rules when the application boots. */ @@ -15,6 +14,7 @@ class CustomValidationServiceProvider extends ServiceProvider { Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) { $validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp']; + return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions); }); @@ -22,6 +22,7 @@ class CustomValidationServiceProvider extends ServiceProvider $cleanLinkName = strtolower(trim($value)); $isJs = strpos($cleanLinkName, 'javascript:') === 0; $isData = strpos($cleanLinkName, 'data:') === 0; + return !$isJs && !$isData; }); } diff --git a/app/Providers/PaginationServiceProvider.php b/app/Providers/PaginationServiceProvider.php index 1c982b82e..416aa5f34 100644 --- a/app/Providers/PaginationServiceProvider.php +++ b/app/Providers/PaginationServiceProvider.php @@ -1,11 +1,12 @@ -mapWebRoutes(); $this->mapApiRoutes(); } + /** * Define the "web" routes for the application. * @@ -47,11 +48,12 @@ class RouteServiceProvider extends ServiceProvider { Route::group([ 'middleware' => 'web', - 'namespace' => $this->namespace, + 'namespace' => $this->namespace, ], function ($router) { require base_path('routes/web.php'); }); } + /** * Define the "api" routes for the application. * @@ -63,8 +65,8 @@ class RouteServiceProvider extends ServiceProvider { Route::group([ 'middleware' => 'api', - 'namespace' => $this->namespace . '\Api', - 'prefix' => 'api', + 'namespace' => $this->namespace . '\Api', + 'prefix' => 'api', ], function ($router) { require base_path('routes/api.php'); }); diff --git a/app/Providers/ThemeServiceProvider.php b/app/Providers/ThemeServiceProvider.php index c41a15af0..54c83884a 100644 --- a/app/Providers/ThemeServiceProvider.php +++ b/app/Providers/ThemeServiceProvider.php @@ -16,7 +16,7 @@ class ThemeServiceProvider extends ServiceProvider public function register() { $this->app->singleton(ThemeService::class, function ($app) { - return new ThemeService; + return new ThemeService(); }); } diff --git a/app/Providers/TranslationServiceProvider.php b/app/Providers/TranslationServiceProvider.php index b7fb9b117..3610a1e22 100644 --- a/app/Providers/TranslationServiceProvider.php +++ b/app/Providers/TranslationServiceProvider.php @@ -1,14 +1,16 @@ -getValueFromStore($key) ?? $default; $formatted = $this->formatValue($value, $default); $this->localCache[$key] = $formatted; + return $formatted; } @@ -51,6 +54,7 @@ class SettingService protected function getFromSession(string $key, $default = false) { $value = session()->get($key, $default); + return $this->formatValue($value, $default); } @@ -66,6 +70,7 @@ class SettingService if ($user->isDefault()) { return $this->getFromSession($key, $default); } + return $this->get($this->userKey($user->id, $key), $default); } @@ -101,6 +106,7 @@ class SettingService } $this->cache->forever($cacheKey, $value); + return $value; } @@ -120,14 +126,14 @@ class SettingService } /** - * Format a settings value + * Format a settings value. */ protected function formatValue($value, $default) { // Change string booleans to actual booleans if ($value === 'true') { $value = true; - } else if ($value === 'false') { + } elseif ($value === 'false') { $value = false; } @@ -135,6 +141,7 @@ class SettingService if ($value === '') { $value = $default; } + return $value; } @@ -144,6 +151,7 @@ class SettingService public function has(string $key): bool { $setting = $this->getSettingObjectByKey($key); + return $setting !== null; } @@ -154,7 +162,7 @@ class SettingService public function put(string $key, $value): bool { $setting = $this->setting->newQuery()->firstOrNew([ - 'setting_key' => $key + 'setting_key' => $key, ]); $setting->type = 'string'; @@ -166,6 +174,7 @@ class SettingService $setting->value = $value; $setting->save(); $this->clearFromCache($key); + return true; } @@ -179,6 +188,7 @@ class SettingService $values = collect($value)->values()->filter(function (array $item) { return count(array_filter($item)) > 0; }); + return json_encode($values); } @@ -189,6 +199,7 @@ class SettingService { if ($user->isDefault()) { session()->put($key, $value); + return true; } diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 56e1fba1c..1965556a9 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -1,4 +1,6 @@ -make(SocialAuthService::class); $socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect); } -} \ No newline at end of file +} diff --git a/app/Traits/HasCreatorAndUpdater.php b/app/Traits/HasCreatorAndUpdater.php index ace1fa12c..a48936bc8 100644 --- a/app/Traits/HasCreatorAndUpdater.php +++ b/app/Traits/HasCreatorAndUpdater.php @@ -1,4 +1,6 @@ -loadPath($themePath, $locale, $group) : []; - $originalTranslations = $this->loadPath($this->path, $locale, $group); + $originalTranslations = $this->loadPath($this->path, $locale, $group); + return array_merge($originalTranslations, $themeTranslations); } diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php index 383af9537..5acd4f141 100644 --- a/app/Uploads/Attachment.php +++ b/app/Uploads/Attachment.php @@ -1,4 +1,6 @@ -name, '.') !== false) { return $this->name; } + return $this->name . '.' . $this->extension; } @@ -47,6 +51,7 @@ class Attachment extends Model if ($this->external && strpos($this->path, 'http') !== 0) { return $this->path; } + return url('/attachments/' . $this->id . ($openInline ? '?open=true' : '')); } @@ -55,7 +60,7 @@ class Attachment extends Model */ public function htmlLink(): string { - return ''.e($this->name).''; + return '' . e($this->name) . ''; } /** @@ -63,6 +68,6 @@ class Attachment extends Model */ public function markdownLink(): string { - return '['. $this->name .']('. $this->getUrl() .')'; + return '[' . $this->name . '](' . $this->getUrl() . ')'; } } diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php index 37adb4f83..298d53a04 100644 --- a/app/Uploads/AttachmentService.php +++ b/app/Uploads/AttachmentService.php @@ -1,4 +1,6 @@ -fileSystem = $fileSystem; } - /** * Get the storage that will be used for storing files. */ @@ -40,6 +40,7 @@ class AttachmentService /** * Get an attachment from storage. + * * @throws FileNotFoundException */ public function getAttachmentFromStorage(Attachment $attachment): string @@ -49,10 +50,13 @@ class AttachmentService /** * Store a new attachment upon user upload. + * * @param UploadedFile $uploadedFile - * @param int $page_id - * @return Attachment + * @param int $page_id + * * @throws FileUploadException + * + * @return Attachment */ public function saveNewUpload(UploadedFile $uploadedFile, $page_id) { @@ -61,13 +65,13 @@ class AttachmentService $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order'); $attachment = Attachment::forceCreate([ - 'name' => $attachmentName, - 'path' => $attachmentPath, - 'extension' => $uploadedFile->getClientOriginalExtension(), + 'name' => $attachmentName, + 'path' => $attachmentPath, + 'extension' => $uploadedFile->getClientOriginalExtension(), 'uploaded_to' => $page_id, - 'created_by' => user()->id, - 'updated_by' => user()->id, - 'order' => $largestExistingOrder + 1 + 'created_by' => user()->id, + 'updated_by' => user()->id, + 'order' => $largestExistingOrder + 1, ]); return $attachment; @@ -76,10 +80,13 @@ class AttachmentService /** * Store a upload, saving to a file and deleting any existing uploads * attached to that file. + * * @param UploadedFile $uploadedFile - * @param Attachment $attachment - * @return Attachment + * @param Attachment $attachment + * * @throws FileUploadException + * + * @return Attachment */ public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment) { @@ -95,6 +102,7 @@ class AttachmentService $attachment->external = false; $attachment->extension = $uploadedFile->getClientOriginalExtension(); $attachment->save(); + return $attachment; } @@ -104,15 +112,16 @@ class AttachmentService public function saveNewFromLink(string $name, string $link, int $page_id): Attachment { $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order'); + return Attachment::forceCreate([ - 'name' => $name, - 'path' => $link, - 'external' => true, - 'extension' => '', + 'name' => $name, + 'path' => $link, + 'external' => true, + 'extension' => '', 'uploaded_to' => $page_id, - 'created_by' => user()->id, - 'updated_by' => user()->id, - 'order' => $largestExistingOrder + 1 + 'created_by' => user()->id, + 'updated_by' => user()->id, + 'order' => $largestExistingOrder + 1, ]); } @@ -128,7 +137,6 @@ class AttachmentService } } - /** * Update the details of a file. */ @@ -145,21 +153,25 @@ class AttachmentService } $attachment->save(); + return $attachment; } /** * Delete a File from the database and storage. + * * @param Attachment $attachment + * * @throws Exception */ public function deleteFile(Attachment $attachment) { if ($attachment->external) { $attachment->delete(); + return; } - + $this->deleteFileInStorage($attachment); $attachment->delete(); } @@ -167,6 +179,7 @@ class AttachmentService /** * Delete a file from the filesystem it sits on. * Cleans any empty leftover folders. + * * @param Attachment $attachment */ protected function deleteFileInStorage(Attachment $attachment) @@ -181,17 +194,20 @@ class AttachmentService } /** - * Store a file in storage with the given filename + * Store a file in storage with the given filename. + * * @param UploadedFile $uploadedFile - * @return string + * * @throws FileUploadException + * + * @return string */ protected function putFileInStorage(UploadedFile $uploadedFile) { $attachmentData = file_get_contents($uploadedFile->getRealPath()); $storage = $this->getStorage(); - $basePath = 'uploads/files/' . Date('Y-m-M') . '/'; + $basePath = 'uploads/files/' . date('Y-m-M') . '/'; $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension(); while ($storage->exists($basePath . $uploadFileName)) { @@ -199,10 +215,12 @@ class AttachmentService } $attachmentPath = $basePath . $uploadFileName; + try { $storage->put($attachmentPath, $attachmentData); } catch (Exception $e) { Log::error('Error when attempting file upload:' . $e->getMessage()); + throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath])); } diff --git a/app/Uploads/HttpFetcher.php b/app/Uploads/HttpFetcher.php index 5e8115637..4198bb2a3 100644 --- a/app/Uploads/HttpFetcher.php +++ b/app/Uploads/HttpFetcher.php @@ -1,23 +1,27 @@ - $uri, + CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, - CURLOPT_CONNECTTIMEOUT => 5 + CURLOPT_CONNECTTIMEOUT => 5, ]); $data = curl_exec($ch); diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 3657aa946..4e0abc85b 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -1,18 +1,20 @@ -page = $page; } - /** * Get an image with the given id. */ @@ -54,8 +54,8 @@ class ImageRepo }); return [ - 'images' => $returnImages, - 'has_more' => $hasMore + 'images' => $returnImages, + 'has_more' => $hasMore, ]; } @@ -121,39 +121,45 @@ class ImageRepo /** * Save a new image into storage and return the new image. + * * @throws ImageUploadException */ public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image { $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio); $this->loadThumbs($image); + return $image; } /** * Save a new image from an existing image data string. + * * @throws ImageUploadException */ public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0) { $image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo); $this->loadThumbs($image); + return $image; } /** * Save a drawing the the database. + * * @throws ImageUploadException */ public function saveDrawing(string $base64Uri, int $uploadedTo): Image { $name = 'Drawing-' . strval(user()->id) . '-' . strval(time()) . '.png'; + return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo); } - /** * Update the details of an image via an array of properties. + * * @throws ImageUploadException * @throws Exception */ @@ -162,11 +168,13 @@ class ImageRepo $image->fill($updateDetails); $image->save(); $this->loadThumbs($image); + return $image; } /** * Destroys an Image object along with its revisions, files and thumbnails. + * * @throws Exception */ public function destroyImage(Image $image = null): bool @@ -174,11 +182,13 @@ class ImageRepo if ($image) { $this->imageService->destroy($image); } + return true; } /** * Destroy all images of a certain type. + * * @throws Exception */ public function destroyByType(string $imageType) @@ -189,16 +199,16 @@ class ImageRepo } } - /** * Load thumbnails onto an image object. + * * @throws Exception */ public function loadThumbs(Image $image) { $image->thumbs = [ 'gallery' => $this->getThumbnail($image, 150, 150, false), - 'display' => $this->getThumbnail($image, 1680, null, true) + 'display' => $this->getThumbnail($image, 1680, null, true), ]; } @@ -206,6 +216,7 @@ 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. + * * @throws Exception */ protected function getThumbnail(Image $image, ?int $width = 220, ?int $height = 220, bool $keepRatio = false): ?string diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 293049f4f..51ddf9bdc 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -1,4 +1,6 @@ -saveNew($name, $data, $type, $uploadedTo); } /** * Save a new image into storage. + * * @throws ImageUploadException */ public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image @@ -95,7 +102,7 @@ class ImageService $secureUploads = setting('app-secure-images'); $fileName = $this->cleanImageFileName($imageName); - $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/'; + $imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/'; while ($storage->exists($imagePath . $fileName)) { $fileName = Str::random(3) . $fileName; @@ -110,15 +117,16 @@ class ImageService $this->saveImageDataInPublicSpace($storage, $fullPath, $imageData); } catch (Exception $e) { \Log::error('Error when attempting image upload:' . $e->getMessage()); + throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath])); } $imageDetails = [ - 'name' => $imageName, - 'path' => $fullPath, - 'url' => $this->getPublicUrl($fullPath), - 'type' => $type, - 'uploaded_to' => $uploadedTo + 'name' => $imageName, + 'path' => $fullPath, + 'url' => $this->getPublicUrl($fullPath), + 'type' => $type, + 'uploaded_to' => $uploadedTo, ]; if (user()->id !== 0) { @@ -129,6 +137,7 @@ class ImageService $image = $this->image->newInstance(); $image->forceFill($imageDetails)->save(); + return $image; } @@ -181,13 +190,16 @@ class ImageService * 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 + * @param int $width + * @param int $height + * @param bool $keepRatio + * * @throws Exception * @throws ImageUploadException + * + * @return string */ public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { @@ -213,18 +225,20 @@ class ImageService $this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData); $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72); - return $this->getPublicUrl($thumbFilePath); } /** * Resize image data. + * * @param string $imageData - * @param int $width - * @param int $height - * @param bool $keepRatio - * @return string + * @param int $width + * @param int $height + * @param bool $keepRatio + * * @throws ImageUploadException + * + * @return string */ protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true) { @@ -234,6 +248,7 @@ class ImageService if ($e instanceof ErrorException || $e instanceof NotSupportedException) { throw new ImageUploadException(trans('errors.cannot_create_thumbs')); } + throw $e; } @@ -246,7 +261,7 @@ class ImageService $thumb->fit($width, $height); } - $thumbData = (string)$thumb->encode(); + $thumbData = (string) $thumb->encode(); // Use original image data if we're keeping the ratio // and the resizing does not save any space. @@ -259,17 +274,20 @@ class ImageService /** * Get the raw data content from an image. + * * @throws FileNotFoundException */ public function getImageData(Image $image): string { $imagePath = $image->path; $storage = $this->getStorage(); + return $storage->get($imagePath); } /** * Destroy an image along with its revisions, thumbnails and remaining folders. + * * @throws Exception */ public function destroy(Image $image) @@ -314,7 +332,8 @@ class ImageService { $files = $storage->files($path); $folders = $storage->directories($path); - return (count($files) === 0 && count($folders) === 0); + + return count($files) === 0 && count($folders) === 0; } /** @@ -350,6 +369,7 @@ class ImageService } } }); + return $deletedPaths; } @@ -358,6 +378,7 @@ class ImageService * Attempts to convert the URL to a system storage url then * fetch the data from the disk or storage location. * Returns null if the image data cannot be fetched from storage. + * * @throws FileNotFoundException */ public function imageUriToBase64(string $uri): ?string @@ -400,6 +421,7 @@ class ImageService if (strpos(strtolower($url), 'uploads/images') === 0) { return trim($url, '/'); } + return null; } @@ -443,6 +465,7 @@ class ImageService } $basePath = ($this->storageUrl == false) ? url('/') : $this->storageUrl; + return rtrim($basePath, '/') . $filePath; } } diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index e98c1cfca..f5b085a35 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -1,4 +1,6 @@ -email)); $replacements = [ - '${hash}' => md5($email), - '${size}' => $size, + '${hash}' => md5($email), + '${size}' => $size, '${email}' => urlencode($email), ]; $userAvatarUrl = strtr($avatarUrl, $replacements); $imageData = $this->getAvatarImageData($userAvatarUrl); + return $this->createAvatarImageFromData($user, $imageData, 'png'); } @@ -101,6 +105,7 @@ class UserAvatars /** * Gets an image from url and returns it as a string of image data. + * * @throws Exception */ protected function getAvatarImageData(string $url): string @@ -110,6 +115,7 @@ class UserAvatars } catch (HttpFetchException $exception) { throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); } + return $imageData; } @@ -119,6 +125,7 @@ class UserAvatars protected function avatarFetchEnabled(): bool { $fetchUrl = $this->getAvatarUrl(); + return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0; } diff --git a/app/Util/HtmlContentFilter.php b/app/Util/HtmlContentFilter.php index 11be5a099..f251a22fd 100644 --- a/app/Util/HtmlContentFilter.php +++ b/app/Util/HtmlContentFilter.php @@ -1,4 +1,6 @@ -query('//@*[starts-with(name(), \'on\')]'); foreach ($onAttributes as $attr) { - /** @var \DOMAttr $attr*/ + /** @var \DOMAttr $attr */ $attrName = $attr->nodeName; $attr->parentNode->removeAttribute($attrName); } diff --git a/app/helpers.php b/app/helpers.php index a5a04f113..9edc22c40 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -7,6 +7,7 @@ use BookStack\Settings\SettingService; /** * Get the path to a versioned file. + * * @throws Exception */ function versioned_asset(string $file = ''): string @@ -24,6 +25,7 @@ function versioned_asset(string $file = ''): string } $path = $file . '?version=' . urlencode($version) . $additional; + return url($path); } @@ -64,6 +66,7 @@ function userCan(string $permission, Model $ownable = null): bool // Check permission on ownable item $permissionService = app(PermissionService::class); + return $permissionService->checkOwnableUserAccess($ownable, $permission); } @@ -74,11 +77,13 @@ function userCan(string $permission, Model $ownable = null): bool function userCanOnAny(string $permission, string $entityClass = null): bool { $permissionService = app(PermissionService::class); + return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass); } /** * Helper to access system settings. + * * @return mixed|SettingService */ function setting(string $key = null, $default = null) @@ -105,7 +110,7 @@ function theme_path(string $path = ''): ?string return null; } - return base_path('themes/' . $theme .($path ? DIRECTORY_SEPARATOR.$path : $path)); + return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path)); } /** @@ -124,7 +129,7 @@ function icon(string $name, array $attrs = []): string ], $attrs); $attrString = ' '; foreach ($attrs as $attrName => $attr) { - $attrString .= $attrName . '="' . $attr . '" '; + $attrString .= $attrName . '="' . $attr . '" '; } $iconPath = resource_path('icons/' . $name . '.svg'); @@ -132,11 +137,12 @@ function icon(string $name, array $attrs = []): string if ($themeIconPath && file_exists($themeIconPath)) { $iconPath = $themeIconPath; - } else if (!file_exists($iconPath)) { + } elseif (!file_exists($iconPath)) { return ''; } $fileContents = file_get_contents($iconPath); + return str_replace('= 7" }, "require-dev": { "phpunit/phpunit": "4.*|5.*", @@ -2760,7 +3016,7 @@ "issues": "https://github.com/paragonie/random_compat/issues", "source": "https://github.com/paragonie/random_compat" }, - "time": "2018-07-02T15:55:56+00:00" + "time": "2020-10-15T08:29:30+00:00" }, { "name": "phenx/php-font-lib", @@ -2951,29 +3207,29 @@ }, { "name": "phpoption/phpoption", - "version": "1.7.5", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" + "reference": "5455cb38aed4523f99977c4a12ef19da4bfe2a28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", - "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/5455cb38aed4523f99977c4a12ef19da4bfe2a28", + "reference": "5455cb38aed4523f99977c4a12ef19da4bfe2a28", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0" + "php": "^7.0 || ^8.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4.1", - "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^6.5.14 || ^7.0.20 || ^8.5.19 || ^9.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.8-dev" } }, "autoload": { @@ -2992,7 +3248,7 @@ }, { "name": "Graham Campbell", - "email": "graham@alt-three.com" + "email": "hello@gjcampbell.co.uk" } ], "description": "Option Type for PHP", @@ -3004,7 +3260,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.7.5" + "source": "https://github.com/schmittjoh/php-option/tree/1.8.0" }, "funding": [ { @@ -3016,7 +3272,59 @@ "type": "tidelift" } ], - "time": "2020-07-20T17:29:33+00:00" + "time": "2021-08-28T21:27:29+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/26c4c5cf30a2844ba121760fd7301f8ad240100b", + "reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.18", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/8.0.0" + }, + "time": "2020-04-05T10:47:18+00:00" }, { "name": "predis/predis", @@ -3384,21 +3692,21 @@ }, { "name": "ramsey/uuid", - "version": "3.9.3", + "version": "3.9.4", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92" + "reference": "be2451bef8147b7352a28fb4cddb08adc497ada3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92", - "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/be2451bef8147b7352a28fb4cddb08adc497ada3", + "reference": "be2451bef8147b7352a28fb4cddb08adc497ada3", "shasum": "" }, "require": { "ext-json": "*", - "paragonie/random_compat": "^1 | ^2 | 9.99.99", + "paragonie/random_compat": "^1 | ^2 | ^9.99.99", "php": "^5.4 | ^7 | ^8", "symfony/polyfill-ctype": "^1.8" }, @@ -3473,7 +3781,17 @@ "source": "https://github.com/ramsey/uuid", "wiki": "https://github.com/ramsey/uuid/wiki" }, - "time": "2020-02-21T04:36:14+00:00" + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2021-08-06T20:32:15+00:00" }, { "name": "robrichards/xmlseclibs", @@ -4084,16 +4402,16 @@ }, { "name": "symfony/console", - "version": "v4.4.29", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b" + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b", - "reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b", + "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22", + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22", "shasum": "" }, "require": { @@ -4154,7 +4472,7 @@ "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.29" + "source": "https://github.com/symfony/console/tree/v4.4.30" }, "funding": [ { @@ -4170,7 +4488,7 @@ "type": "tidelift" } ], - "time": "2021-07-27T19:04:53+00:00" + "time": "2021-08-25T19:27:26+00:00" }, { "name": "symfony/css-selector", @@ -4375,16 +4693,16 @@ }, { "name": "symfony/error-handler", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "16ac2be1c0f49d6d9eb9d3ce9324bde268717905" + "reference": "51f98f7aa99f00f3b1da6bafe934e67ae6ba6dc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/16ac2be1c0f49d6d9eb9d3ce9324bde268717905", - "reference": "16ac2be1c0f49d6d9eb9d3ce9324bde268717905", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/51f98f7aa99f00f3b1da6bafe934e67ae6ba6dc5", + "reference": "51f98f7aa99f00f3b1da6bafe934e67ae6ba6dc5", "shasum": "" }, "require": { @@ -4423,7 +4741,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v4.4.27" + "source": "https://github.com/symfony/error-handler/tree/v4.4.30" }, "funding": [ { @@ -4439,20 +4757,20 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-27T17:42:48+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9" + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/958a128b184fcf0ba45ec90c0e88554c9327c2e9", - "reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac", + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac", "shasum": "" }, "require": { @@ -4507,7 +4825,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.27" + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30" }, "funding": [ { @@ -4523,7 +4841,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4606,16 +4924,16 @@ }, { "name": "symfony/finder", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "42414d7ac96fc2880a783b872185789dea0d4262" + "reference": "70362f1e112280d75b30087c7598b837c1b468b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/42414d7ac96fc2880a783b872185789dea0d4262", - "reference": "42414d7ac96fc2880a783b872185789dea0d4262", + "url": "https://api.github.com/repos/symfony/finder/zipball/70362f1e112280d75b30087c7598b837c1b468b6", + "reference": "70362f1e112280d75b30087c7598b837c1b468b6", "shasum": "" }, "require": { @@ -4648,7 +4966,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v4.4.27" + "source": "https://github.com/symfony/finder/tree/v4.4.30" }, "funding": [ { @@ -4664,7 +4982,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/http-client-contracts", @@ -4746,16 +5064,16 @@ }, { "name": "symfony/http-foundation", - "version": "v4.4.29", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "7016057b01f0ed3ec3ba1f31a580b6661667c2e1" + "reference": "09b3202651ab23ac8dcf455284a48a3500e56731" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7016057b01f0ed3ec3ba1f31a580b6661667c2e1", - "reference": "7016057b01f0ed3ec3ba1f31a580b6661667c2e1", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/09b3202651ab23ac8dcf455284a48a3500e56731", + "reference": "09b3202651ab23ac8dcf455284a48a3500e56731", "shasum": "" }, "require": { @@ -4794,7 +5112,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v4.4.29" + "source": "https://github.com/symfony/http-foundation/tree/v4.4.30" }, "funding": [ { @@ -4810,20 +5128,20 @@ "type": "tidelift" } ], - "time": "2021-07-27T14:32:23+00:00" + "time": "2021-08-26T15:51:23+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.4.29", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "752b170e1ba0dd4104e7fa17c1cef1ec8a7fc506" + "reference": "87f7ea4a8a7a30c967e26001de99f12943bf57ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/752b170e1ba0dd4104e7fa17c1cef1ec8a7fc506", - "reference": "752b170e1ba0dd4104e7fa17c1cef1ec8a7fc506", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/87f7ea4a8a7a30c967e26001de99f12943bf57ae", + "reference": "87f7ea4a8a7a30c967e26001de99f12943bf57ae", "shasum": "" }, "require": { @@ -4832,7 +5150,7 @@ "symfony/error-handler": "^4.4", "symfony/event-dispatcher": "^4.4", "symfony/http-client-contracts": "^1.1|^2", - "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-foundation": "^4.4.30|^5.3.7", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", "symfony/polyfill-php80": "^1.16" @@ -4898,7 +5216,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v4.4.29" + "source": "https://github.com/symfony/http-kernel/tree/v4.4.30" }, "funding": [ { @@ -4914,20 +5232,20 @@ "type": "tidelift" } ], - "time": "2021-07-29T06:45:05+00:00" + "time": "2021-08-30T12:27:20+00:00" }, { "name": "symfony/mime", - "version": "v5.3.4", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "633e4e8afe9e529e5599d71238849a4218dd497b" + "reference": "ae887cb3b044658676129f5e97aeb7e9eb69c2d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/633e4e8afe9e529e5599d71238849a4218dd497b", - "reference": "633e4e8afe9e529e5599d71238849a4218dd497b", + "url": "https://api.github.com/repos/symfony/mime/zipball/ae887cb3b044658676129f5e97aeb7e9eb69c2d8", + "reference": "ae887cb3b044658676129f5e97aeb7e9eb69c2d8", "shasum": "" }, "require": { @@ -4981,7 +5299,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.3.4" + "source": "https://github.com/symfony/mime/tree/v5.3.7" }, "funding": [ { @@ -4997,7 +5315,7 @@ "type": "tidelift" } ], - "time": "2021-07-21T12:40:44+00:00" + "time": "2021-08-20T11:40:01+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5649,16 +5967,16 @@ }, { "name": "symfony/process", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f" + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f", - "reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f", + "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", "shasum": "" }, "require": { @@ -5691,7 +6009,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.27" + "source": "https://github.com/symfony/process/tree/v4.4.30" }, "funding": [ { @@ -5707,20 +6025,20 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/routing", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "244609821beece97167fa7ba4eef49d2a31862db" + "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/244609821beece97167fa7ba4eef49d2a31862db", - "reference": "244609821beece97167fa7ba4eef49d2a31862db", + "url": "https://api.github.com/repos/symfony/routing/zipball/9ddf033927ad9f30ba2bfd167a7b342cafa13e8e", + "reference": "9ddf033927ad9f30ba2bfd167a7b342cafa13e8e", "shasum": "" }, "require": { @@ -5780,7 +6098,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v4.4.27" + "source": "https://github.com/symfony/routing/tree/v4.4.30" }, "funding": [ { @@ -5796,7 +6114,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T21:41:01+00:00" }, { "name": "symfony/service-contracts", @@ -5879,16 +6197,16 @@ }, { "name": "symfony/translation", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "2e3c0f2bf704d635ba862e7198d72331a62d82ba" + "reference": "db0ba1e85280d8ff11e38d53c70f8814d4d740f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/2e3c0f2bf704d635ba862e7198d72331a62d82ba", - "reference": "2e3c0f2bf704d635ba862e7198d72331a62d82ba", + "url": "https://api.github.com/repos/symfony/translation/zipball/db0ba1e85280d8ff11e38d53c70f8814d4d740f5", + "reference": "db0ba1e85280d8ff11e38d53c70f8814d4d740f5", "shasum": "" }, "require": { @@ -5948,7 +6266,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v4.4.27" + "source": "https://github.com/symfony/translation/tree/v4.4.30" }, "funding": [ { @@ -5964,7 +6282,7 @@ "type": "tidelift" } ], - "time": "2021-07-21T13:12:00+00:00" + "time": "2021-08-26T05:57:13+00:00" }, { "name": "symfony/translation-contracts", @@ -6046,16 +6364,16 @@ }, { "name": "symfony/var-dumper", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba" + "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/391d6d0e7a06ab54eb7c38fab29b8d174471b3ba", - "reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c", + "reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c", "shasum": "" }, "require": { @@ -6115,7 +6433,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v4.4.27" + "source": "https://github.com/symfony/var-dumper/tree/v4.4.30" }, "funding": [ { @@ -6131,7 +6449,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -6566,16 +6884,16 @@ }, { "name": "composer/composer", - "version": "2.1.5", + "version": "2.1.6", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "ac679902e9f66b85a8f9d8c1c88180f609a8745d" + "reference": "e5cac5f9d2354d08b67f1d21c664ae70d748c603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/ac679902e9f66b85a8f9d8c1c88180f609a8745d", - "reference": "ac679902e9f66b85a8f9d8c1c88180f609a8745d", + "url": "https://api.github.com/repos/composer/composer/zipball/e5cac5f9d2354d08b67f1d21c664ae70d748c603", + "reference": "e5cac5f9d2354d08b67f1d21c664ae70d748c603", "shasum": "" }, "require": { @@ -6642,9 +6960,9 @@ "package" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.1.5" + "source": "https://github.com/composer/composer/tree/2.1.6" }, "funding": [ { @@ -6660,7 +6978,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T08:35:47+00:00" + "time": "2021-08-19T15:11:08+00:00" }, { "name": "composer/metadata-minifier", @@ -8182,16 +8500,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.8", + "version": "9.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb" + "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/191768ccd5c85513b4068bdbe99bb6390c7d54fb", - "reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b", + "reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b", "shasum": "" }, "require": { @@ -8269,7 +8587,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.9" }, "funding": [ { @@ -8281,7 +8599,7 @@ "type": "github" } ], - "time": "2021-07-31T15:17:34+00:00" + "time": "2021-08-31T06:47:40+00:00" }, { "name": "react/promise", @@ -9362,16 +9680,16 @@ }, { "name": "seld/phar-utils", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796" + "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796", - "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/749042a2315705d2dfbbc59234dd9ceb22bf3ff0", + "reference": "749042a2315705d2dfbbc59234dd9ceb22bf3ff0", "shasum": "" }, "require": { @@ -9404,78 +9722,22 @@ ], "support": { "issues": "https://github.com/Seldaek/phar-utils/issues", - "source": "https://github.com/Seldaek/phar-utils/tree/master" + "source": "https://github.com/Seldaek/phar-utils/tree/1.1.2" }, - "time": "2020-07-07T18:42:57+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.6.0", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ffced0d2c8fa8e6cdc4d695a743271fab6c38625", - "reference": "ffced0d2c8fa8e6cdc4d695a743271fab6c38625", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.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": "https://github.com/squizlabs/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards" - ], - "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" - }, - "time": "2021-04-09T00:54:41+00:00" + "time": "2021-08-19T21:01:38+00:00" }, { "name": "symfony/dom-crawler", - "version": "v4.4.27", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "86aa075c9e0b13ac7db8d73d1f9d8b656143881a" + "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/86aa075c9e0b13ac7db8d73d1f9d8b656143881a", - "reference": "86aa075c9e0b13ac7db8d73d1f9d8b656143881a", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4632ae3567746c7e915c33c67a2fb6ab746090c4", + "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4", "shasum": "" }, "require": { @@ -9520,7 +9782,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v4.4.27" + "source": "https://github.com/symfony/dom-crawler/tree/v4.4.30" }, "funding": [ { @@ -9536,7 +9798,7 @@ "type": "tidelift" } ], - "time": "2021-07-23T15:41:52+00:00" + "time": "2021-08-28T15:40:01+00:00" }, { "name": "symfony/filesystem", @@ -9729,5 +9991,5 @@ "platform-overrides": { "php": "7.3.0" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 722f68a7c..dc0645540 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -13,81 +13,84 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) { $name = $faker->name; + return [ - 'name' => $name, - 'email' => $faker->email, - 'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)), - 'password' => Str::random(10), - 'remember_token' => Str::random(10), - 'email_confirmed' => 1 + 'name' => $name, + 'email' => $faker->email, + 'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)), + 'password' => Str::random(10), + 'remember_token' => Str::random(10), + 'email_confirmed' => 1, ]; }); $factory->define(\BookStack\Entities\Models\Bookshelf::class, function ($faker) { return [ - 'name' => $faker->sentence, - 'slug' => Str::random(10), - 'description' => $faker->paragraph + 'name' => $faker->sentence, + 'slug' => Str::random(10), + 'description' => $faker->paragraph, ]; }); $factory->define(\BookStack\Entities\Models\Book::class, function ($faker) { return [ - 'name' => $faker->sentence, - 'slug' => Str::random(10), - 'description' => $faker->paragraph + 'name' => $faker->sentence, + 'slug' => Str::random(10), + 'description' => $faker->paragraph, ]; }); $factory->define(\BookStack\Entities\Models\Chapter::class, function ($faker) { return [ - 'name' => $faker->sentence, - 'slug' => Str::random(10), - 'description' => $faker->paragraph + 'name' => $faker->sentence, + 'slug' => Str::random(10), + 'description' => $faker->paragraph, ]; }); $factory->define(\BookStack\Entities\Models\Page::class, function ($faker) { $html = '

      ' . implode('

      ', $faker->paragraphs(5)) . '

      '; + return [ - 'name' => $faker->sentence, - 'slug' => Str::random(10), - 'html' => $html, - 'text' => strip_tags($html), - 'revision_count' => 1 + 'name' => $faker->sentence, + 'slug' => Str::random(10), + 'html' => $html, + 'text' => strip_tags($html), + 'revision_count' => 1, ]; }); $factory->define(\BookStack\Auth\Role::class, function ($faker) { return [ 'display_name' => $faker->sentence(3), - 'description' => $faker->sentence(10) + 'description' => $faker->sentence(10), ]; }); $factory->define(\BookStack\Actions\Tag::class, function ($faker) { return [ - 'name' => $faker->city, - 'value' => $faker->sentence(3) + 'name' => $faker->city, + 'value' => $faker->sentence(3), ]; }); $factory->define(\BookStack\Uploads\Image::class, function ($faker) { return [ - 'name' => $faker->slug . '.jpg', - 'url' => $faker->url, - 'path' => $faker->url, - 'type' => 'gallery', - 'uploaded_to' => 0 + 'name' => $faker->slug . '.jpg', + 'url' => $faker->url, + 'path' => $faker->url, + 'type' => 'gallery', + 'uploaded_to' => 0, ]; }); -$factory->define(\BookStack\Actions\Comment::class, function($faker) { +$factory->define(\BookStack\Actions\Comment::class, function ($faker) { $text = $faker->paragraph(1); - $html = '

      ' . $text. '

      '; + $html = '

      ' . $text . '

      '; + return [ - 'html' => $html, - 'text' => $text, - 'parent_id' => null + 'html' => $html, + 'text' => $text, + 'parent_id' => null, ]; -}); \ No newline at end of file +}); diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index 17e71de5f..10ae5222b 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -1,7 +1,7 @@ insert([ - 'name' => 'Admin', - 'email' => 'admin@admin.com', - 'password' => bcrypt('password'), + 'name' => 'Admin', + 'email' => 'admin@admin.com', + 'password' => bcrypt('password'), 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); } diff --git a/database/migrations/2014_10_12_100000_create_password_resets_table.php b/database/migrations/2014_10_12_100000_create_password_resets_table.php index 00057f9cf..c647b562d 100644 --- a/database/migrations/2014_10_12_100000_create_password_resets_table.php +++ b/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -1,7 +1,7 @@ increments('id'); $table->integer('book_id'); diff --git a/database/migrations/2015_07_13_172121_create_images_table.php b/database/migrations/2015_07_13_172121_create_images_table.php index 61beaa7c3..f54ab9e2a 100644 --- a/database/migrations/2015_07_13_172121_create_images_table.php +++ b/database/migrations/2015_07_13_172121_create_images_table.php @@ -1,7 +1,7 @@ primary(['permission_id', 'role_id']); }); - // Create default roles $adminId = DB::table('roles')->insertGetId([ - 'name' => 'admin', + 'name' => 'admin', 'display_name' => 'Admin', - 'description' => 'Administrator of the whole application', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'description' => 'Administrator of the whole application', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); $editorId = DB::table('roles')->insertGetId([ - 'name' => 'editor', + 'name' => 'editor', 'display_name' => 'Editor', - 'description' => 'User can edit Books, Chapters & Pages', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'description' => 'User can edit Books, Chapters & Pages', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); $viewerId = DB::table('roles')->insertGetId([ - 'name' => 'viewer', + 'name' => 'viewer', 'display_name' => 'Viewer', - 'description' => 'User can view books & their content behind authentication', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'description' => 'User can view books & their content behind authentication', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); - // Create default CRUD permissions and allocate to admins and editors $entities = ['Book', 'Page', 'Chapter', 'Image']; $ops = ['Create', 'Update', 'Delete']; foreach ($entities as $entity) { foreach ($ops as $op) { $newPermId = DB::table('permissions')->insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower($op), + 'name' => strtolower($entity) . '-' . strtolower($op), 'display_name' => $op . ' ' . $entity . 's', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ ['permission_id' => $newPermId, 'role_id' => $adminId], - ['permission_id' => $newPermId, 'role_id' => $editorId] + ['permission_id' => $newPermId, 'role_id' => $editorId], ]); } } @@ -115,14 +112,14 @@ class AddRolesAndPermissions extends Migration foreach ($entities as $entity) { foreach ($ops as $op) { $newPermId = DB::table('permissions')->insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower($op), + 'name' => strtolower($entity) . '-' . strtolower($op), 'display_name' => $op . ' ' . $entity, - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ 'permission_id' => $newPermId, - 'role_id' => $adminId + 'role_id' => $adminId, ]); } } @@ -133,10 +130,9 @@ class AddRolesAndPermissions extends Migration foreach ($users as $user) { DB::table('role_user')->insert([ 'role_id' => $adminId, - 'user_id' => $user->id + 'user_id' => $user->id, ]); } - } /** diff --git a/database/migrations/2015_08_30_125859_create_settings_table.php b/database/migrations/2015_08_30_125859_create_settings_table.php index 2cef3e6e7..1b0a8c1c6 100644 --- a/database/migrations/2015_08_30_125859_create_settings_table.php +++ b/database/migrations/2015_08_30_125859_create_settings_table.php @@ -1,7 +1,7 @@ listTableDetails('chapters'); if ($pages->hasIndex('search')) { - Schema::table('pages', function(Blueprint $table) { + Schema::table('pages', function (Blueprint $table) { $table->dropIndex('search'); }); } if ($books->hasIndex('search')) { - Schema::table('books', function(Blueprint $table) { + Schema::table('books', function (Blueprint $table) { $table->dropIndex('search'); }); } if ($chapters->hasIndex('search')) { - Schema::table('chapters', function(Blueprint $table) { + Schema::table('chapters', function (Blueprint $table) { $table->dropIndex('search'); }); } - } } diff --git a/database/migrations/2015_09_04_165821_create_social_accounts_table.php b/database/migrations/2015_09_04_165821_create_social_accounts_table.php index 700d7f90f..3eec96163 100644 --- a/database/migrations/2015_09_04_165821_create_social_accounts_table.php +++ b/database/migrations/2015_09_04_165821_create_social_accounts_table.php @@ -1,7 +1,7 @@ listTableDetails('chapters'); if ($pages->hasIndex('name_search')) { - Schema::table('pages', function(Blueprint $table) { + Schema::table('pages', function (Blueprint $table) { $table->dropIndex('name_search'); }); } if ($books->hasIndex('name_search')) { - Schema::table('books', function(Blueprint $table) { + Schema::table('books', function (Blueprint $table) { $table->dropIndex('name_search'); }); } if ($chapters->hasIndex('name_search')) { - Schema::table('chapters', function(Blueprint $table) { + Schema::table('chapters', function (Blueprint $table) { $table->dropIndex('name_search'); }); } diff --git a/database/migrations/2015_12_07_195238_add_image_upload_types.php b/database/migrations/2015_12_07_195238_add_image_upload_types.php index 515bc9d8d..3ebb10bb9 100644 --- a/database/migrations/2015_12_07_195238_add_image_upload_types.php +++ b/database/migrations/2015_12_07_195238_add_image_upload_types.php @@ -1,8 +1,8 @@ string('type')->index(); }); - Image::all()->each(function($image) { + Image::all()->each(function ($image) { $image->path = $image->url; $image->type = 'gallery'; $image->save(); @@ -36,6 +36,5 @@ class AddImageUploadTypes extends Migration $table->dropColumn('type'); $table->dropColumn('path'); }); - } } diff --git a/database/migrations/2015_12_09_195748_add_user_avatars.php b/database/migrations/2015_12_09_195748_add_user_avatars.php index 47cb027fa..083f0a5bc 100644 --- a/database/migrations/2015_12_09_195748_add_user_avatars.php +++ b/database/migrations/2015_12_09_195748_add_user_avatars.php @@ -1,7 +1,7 @@ dropColumn('external_auth_id'); }); } -} \ No newline at end of file +} diff --git a/database/migrations/2016_02_25_184030_add_slug_to_revisions.php b/database/migrations/2016_02_25_184030_add_slug_to_revisions.php index 0be6c7940..7139178e8 100644 --- a/database/migrations/2016_02_25_184030_add_slug_to_revisions.php +++ b/database/migrations/2016_02_25_184030_add_slug_to_revisions.php @@ -1,7 +1,7 @@ 'Manage Settings', - 'users-manage' => 'Manage Users', - 'user-roles-manage' => 'Manage Roles & Permissions', + 'settings-manage' => 'Manage Settings', + 'users-manage' => 'Manage Users', + 'user-roles-manage' => 'Manage Roles & Permissions', 'restrictions-manage-all' => 'Manage All Entity Permissions', - 'restrictions-manage-own' => 'Manage Entity Permissions On Own Content' + 'restrictions-manage-own' => 'Manage Entity Permissions On Own Content', ]; foreach ($permissionsToCreate as $name => $displayName) { $permissionId = DB::table('permissions')->insertGetId([ - 'name' => $name, + 'name' => $name, 'display_name' => $displayName, - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } @@ -46,24 +45,23 @@ class UpdatePermissionsAndRoles extends Migration foreach ($entities as $entity) { foreach ($ops as $op) { $permissionId = DB::table('permissions')->insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), 'display_name' => $op . ' ' . $entity . 's', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); if ($editorRole !== null) { DB::table('permission_role')->insert([ - 'role_id' => $editorRole->id, - 'permission_id' => $permissionId + 'role_id' => $editorRole->id, + 'permission_id' => $permissionId, ]); } } } - } /** @@ -85,14 +83,14 @@ class UpdatePermissionsAndRoles extends Migration foreach ($entities as $entity) { foreach ($ops as $op) { $permissionId = DB::table('permissions')->insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower($op), + 'name' => strtolower($entity) . '-' . strtolower($op), 'display_name' => $op . ' ' . $entity . 's', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } } @@ -103,14 +101,14 @@ class UpdatePermissionsAndRoles extends Migration foreach ($entities as $entity) { foreach ($ops as $op) { $permissionId = DB::table('permissions')->insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower($op), + 'name' => strtolower($entity) . '-' . strtolower($op), 'display_name' => $op . ' ' . $entity, - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } } diff --git a/database/migrations/2016_02_28_084200_add_entity_access_controls.php b/database/migrations/2016_02_28_084200_add_entity_access_controls.php index 5df2353a2..674640952 100644 --- a/database/migrations/2016_02_28_084200_add_entity_access_controls.php +++ b/database/migrations/2016_02_28_084200_add_entity_access_controls.php @@ -1,7 +1,7 @@ index('restricted'); }); - Schema::create('restrictions', function(Blueprint $table) { + Schema::create('restrictions', function (Blueprint $table) { $table->increments('id'); $table->integer('restrictable_id'); $table->string('restrictable_type'); @@ -63,7 +63,6 @@ class AddEntityAccessControls extends Migration $table->dropColumn('restricted'); }); - Schema::table('pages', function (Blueprint $table) { $table->dropColumn('restricted'); }); diff --git a/database/migrations/2016_03_09_203143_add_page_revision_types.php b/database/migrations/2016_03_09_203143_add_page_revision_types.php index e39c77d18..d633fb949 100644 --- a/database/migrations/2016_03_09_203143_add_page_revision_types.php +++ b/database/migrations/2016_03_09_203143_add_page_revision_types.php @@ -1,7 +1,7 @@ boolean('draft')->default(false); $table->index('draft'); }); diff --git a/database/migrations/2016_03_25_123157_add_markdown_support.php b/database/migrations/2016_03_25_123157_add_markdown_support.php index 2daa32cfb..27a198dc9 100644 --- a/database/migrations/2016_03_25_123157_add_markdown_support.php +++ b/database/migrations/2016_03_25_123157_add_markdown_support.php @@ -1,7 +1,7 @@ insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), 'display_name' => $op . ' ' . $entity . 's', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); // Assign view permission to all current roles foreach ($currentRoles as $role) { DB::table('permission_role')->insert([ - 'role_id' => $role->id, - 'permission_id' => $permId + 'role_id' => $role->id, + 'permission_id' => $permId, ]); } } diff --git a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php index ce11f7b88..5b43c7d54 100644 --- a/database/migrations/2016_04_20_192649_create_joint_permissions_table.php +++ b/database/migrations/2016_04_20_192649_create_joint_permissions_table.php @@ -1,7 +1,7 @@ 'public', + 'name' => 'public', 'display_name' => 'Public', - 'description' => 'The role given to public visitors if allowed', - 'system_name' => 'public', - 'hidden' => true, - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'description' => 'The role given to public visitors if allowed', + 'system_name' => 'public', + 'hidden' => true, + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]; // Ensure unique name @@ -67,7 +67,7 @@ class CreateJointPermissionsTable extends Migration // Assign view permission to public DB::table('permission_role')->insert([ 'permission_id' => $permission->id, - 'role_id' => $publicRoleId + 'role_id' => $publicRoleId, ]); } } diff --git a/database/migrations/2016_05_06_185215_create_tags_table.php b/database/migrations/2016_05_06_185215_create_tags_table.php index 55eed6060..1c691f561 100644 --- a/database/migrations/2016_05_06_185215_create_tags_table.php +++ b/database/migrations/2016_05_06_185215_create_tags_table.php @@ -1,7 +1,7 @@ dropColumn('hidden'); }); // Add column to mark system users - Schema::table('users', function(Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->string('system_name')->nullable()->index(); }); // Insert our new public system user. $publicUserId = DB::table('users')->insertGetId([ - 'email' => 'guest@example.com', - 'name' => 'Guest', - 'system_name' => 'public', + 'email' => 'guest@example.com', + 'name' => 'Guest', + 'system_name' => 'public', 'email_confirmed' => true, - 'created_at' => \Carbon\Carbon::now(), - 'updated_at' => \Carbon\Carbon::now(), + 'created_at' => \Carbon\Carbon::now(), + 'updated_at' => \Carbon\Carbon::now(), ]); - + // Get the public role $publicRole = DB::table('roles')->where('system_name', '=', 'public')->first(); // Connect the new public user to the public role DB::table('role_user')->insert([ 'user_id' => $publicUserId, - 'role_id' => $publicRole->id + 'role_id' => $publicRole->id, ]); } @@ -50,14 +50,14 @@ class RemoveHiddenRoles extends Migration */ public function down() { - Schema::table('roles', function(Blueprint $table) { + Schema::table('roles', function (Blueprint $table) { $table->boolean('hidden')->default(false); $table->index('hidden'); }); DB::table('users')->where('system_name', '=', 'public')->delete(); - Schema::table('users', function(Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->dropColumn('system_name'); }); diff --git a/database/migrations/2016_10_09_142037_create_attachments_table.php b/database/migrations/2016_10_09_142037_create_attachments_table.php index 627c237c4..9c5422f08 100644 --- a/database/migrations/2016_10_09_142037_create_attachments_table.php +++ b/database/migrations/2016_10_09_142037_create_attachments_table.php @@ -1,8 +1,8 @@ insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), 'display_name' => $op . ' ' . $entity . 's', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } - } /** diff --git a/database/migrations/2017_01_21_163556_create_cache_table.php b/database/migrations/2017_01_21_163556_create_cache_table.php index 1f7761c2b..f77d4a4eb 100644 --- a/database/migrations/2017_01_21_163556_create_cache_table.php +++ b/database/migrations/2017_01_21_163556_create_cache_table.php @@ -1,8 +1,8 @@ listTableDetails('chapters'); if ($pages->hasIndex('search')) { - Schema::table('pages', function(Blueprint $table) { + Schema::table('pages', function (Blueprint $table) { $table->dropIndex('search'); $table->dropIndex('name_search'); }); } if ($books->hasIndex('search')) { - Schema::table('books', function(Blueprint $table) { + Schema::table('books', function (Blueprint $table) { $table->dropIndex('search'); $table->dropIndex('name_search'); }); } if ($chapters->hasIndex('search')) { - Schema::table('chapters', function(Blueprint $table) { + Schema::table('chapters', function (Blueprint $table) { $table->dropIndex('search'); $table->dropIndex('name_search'); }); @@ -70,7 +70,7 @@ class CreateSearchIndexTable extends Migration // DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)"); // DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)"); // DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)"); - + Schema::dropIfExists('search_terms'); } } diff --git a/database/migrations/2017_04_20_185112_add_revision_counts.php b/database/migrations/2017_04_20_185112_add_revision_counts.php index 3583f36f3..8c6d75e77 100644 --- a/database/migrations/2017_04_20_185112_add_revision_counts.php +++ b/database/migrations/2017_04_20_185112_add_revision_counts.php @@ -1,8 +1,8 @@ insertGetId([ - 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), 'display_name' => $op . ' ' . $entity . 's', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } - }); } diff --git a/database/migrations/2017_08_29_102650_add_cover_image_display.php b/database/migrations/2017_08_29_102650_add_cover_image_display.php index 6f9932924..7dd924338 100644 --- a/database/migrations/2017_08_29_102650_add_cover_image_display.php +++ b/database/migrations/2017_08_29_102650_add_cover_image_display.php @@ -1,8 +1,8 @@ where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id'); $permId = DB::table('role_permissions')->insertGetId([ - 'name' => 'bookshelf-' . $dbOpName, + 'name' => 'bookshelf-' . $dbOpName, 'display_name' => $op . ' ' . 'BookShelves', - 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), - 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(), ]); - $rowsToInsert = $roleIdsWithBookPermission->filter(function($roleId) { + $rowsToInsert = $roleIdsWithBookPermission->filter(function ($roleId) { return !is_null($roleId); - })->map(function($roleId) use ($permId) { + })->map(function ($roleId) use ($permId) { return [ - 'role_id' => $roleId, - 'permission_id' => $permId + 'role_id' => $roleId, + 'permission_id' => $permId, ]; })->toArray(); @@ -107,7 +108,7 @@ class CreateBookshelvesTable extends Migration public function down() { // Drop created permissions - $ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own']; + $ops = ['bookshelf-create-all', 'bookshelf-create-own', 'bookshelf-delete-all', 'bookshelf-delete-own', 'bookshelf-update-all', 'bookshelf-update-own', 'bookshelf-view-all', 'bookshelf-view-own']; $permissionIds = DB::table('role_permissions')->whereIn('name', $ops) ->get(['id'])->pluck('id')->toArray(); diff --git a/database/migrations/2019_07_07_112515_add_template_support.php b/database/migrations/2019_07_07_112515_add_template_support.php index 3fcc68227..ae26985ed 100644 --- a/database/migrations/2019_07_07_112515_add_template_support.php +++ b/database/migrations/2019_07_07_112515_add_template_support.php @@ -1,9 +1,9 @@ where('system_name', '=', 'admin')->first()->id; $permissionId = DB::table('role_permissions')->insertGetId([ - 'name' => 'templates-manage', + 'name' => 'templates-manage', 'display_name' => 'Manage Page Templates', - 'created_at' => Carbon::now()->toDateTimeString(), - 'updated_at' => Carbon::now()->toDateTimeString() + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } diff --git a/database/migrations/2019_08_17_140214_add_user_invites_table.php b/database/migrations/2019_08_17_140214_add_user_invites_table.php index 23bd6988c..6321b8187 100644 --- a/database/migrations/2019_08_17_140214_add_user_invites_table.php +++ b/database/migrations/2019_08_17_140214_add_user_invites_table.php @@ -1,8 +1,8 @@ increments('id'); $table->string('name'); $table->string('token_id')->unique(); @@ -29,14 +29,14 @@ class AddApiAuth extends Migration // Add access-api permission $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; $permissionId = DB::table('role_permissions')->insertGetId([ - 'name' => 'access-api', + 'name' => 'access-api', 'display_name' => 'Access system API', - 'created_at' => Carbon::now()->toDateTimeString(), - 'updated_at' => Carbon::now()->toDateTimeString() + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), ]); DB::table('permission_role')->insert([ - 'role_id' => $adminRoleId, - 'permission_id' => $permissionId + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId, ]); } diff --git a/database/migrations/2020_08_04_131052_remove_role_name_field.php b/database/migrations/2020_08_04_131052_remove_role_name_field.php index f3cafb732..8f99817d2 100644 --- a/database/migrations/2020_08_04_131052_remove_role_name_field.php +++ b/database/migrations/2020_08_04_131052_remove_role_name_field.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class RemoveRoleNameField extends Migration { @@ -31,7 +31,7 @@ class RemoveRoleNameField extends Migration }); DB::table('roles')->update([ - "name" => DB::raw("lower(replace(`display_name`, ' ', '-'))"), + 'name' => DB::raw("lower(replace(`display_name`, ' ', '-'))"), ]); } } diff --git a/database/migrations/2020_09_19_094251_add_activity_indexes.php b/database/migrations/2020_09_19_094251_add_activity_indexes.php index 7d6a270a9..28355265b 100644 --- a/database/migrations/2020_09_19_094251_add_activity_indexes.php +++ b/database/migrations/2020_09_19_094251_add_activity_indexes.php @@ -13,7 +13,7 @@ class AddActivityIndexes extends Migration */ public function up() { - Schema::table('activities', function(Blueprint $table) { + Schema::table('activities', function (Blueprint $table) { $table->index('key'); $table->index('created_at'); }); @@ -26,7 +26,7 @@ class AddActivityIndexes extends Migration */ public function down() { - Schema::table('activities', function(Blueprint $table) { + Schema::table('activities', function (Blueprint $table) { $table->dropIndex('activities_key_index'); $table->dropIndex('activities_created_at_index'); }); diff --git a/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php b/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php index d2b63e8d0..09ee87f5a 100644 --- a/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php +++ b/database/migrations/2020_09_27_210059_add_entity_soft_deletes.php @@ -13,16 +13,16 @@ class AddEntitySoftDeletes extends Migration */ public function up() { - Schema::table('bookshelves', function(Blueprint $table) { + Schema::table('bookshelves', function (Blueprint $table) { $table->softDeletes(); }); - Schema::table('books', function(Blueprint $table) { + Schema::table('books', function (Blueprint $table) { $table->softDeletes(); }); - Schema::table('chapters', function(Blueprint $table) { + Schema::table('chapters', function (Blueprint $table) { $table->softDeletes(); }); - Schema::table('pages', function(Blueprint $table) { + Schema::table('pages', function (Blueprint $table) { $table->softDeletes(); }); } @@ -34,16 +34,16 @@ class AddEntitySoftDeletes extends Migration */ public function down() { - Schema::table('bookshelves', function(Blueprint $table) { + Schema::table('bookshelves', function (Blueprint $table) { $table->dropSoftDeletes(); }); - Schema::table('books', function(Blueprint $table) { + Schema::table('books', function (Blueprint $table) { $table->dropSoftDeletes(); }); - Schema::table('chapters', function(Blueprint $table) { + Schema::table('chapters', function (Blueprint $table) { $table->dropSoftDeletes(); }); - Schema::table('pages', function(Blueprint $table) { + Schema::table('pages', function (Blueprint $table) { $table->dropSoftDeletes(); }); } diff --git a/database/migrations/2020_11_07_232321_simplify_activities_table.php b/database/migrations/2020_11_07_232321_simplify_activities_table.php index 828dbc656..59f13f456 100644 --- a/database/migrations/2020_11_07_232321_simplify_activities_table.php +++ b/database/migrations/2020_11_07_232321_simplify_activities_table.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class SimplifyActivitiesTable extends Migration { @@ -25,7 +25,7 @@ class SimplifyActivitiesTable extends Migration DB::table('activities') ->where('entity_id', '=', 0) ->update([ - 'entity_id' => null, + 'entity_id' => null, 'entity_type' => null, ]); } @@ -40,7 +40,7 @@ class SimplifyActivitiesTable extends Migration DB::table('activities') ->whereNull('entity_id') ->update([ - 'entity_id' => 0, + 'entity_id' => 0, 'entity_type' => '', ]); diff --git a/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php b/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php index bf8bf281f..abff3906f 100644 --- a/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php +++ b/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class AddOwnedByFieldToEntities extends Migration { diff --git a/database/migrations/2021_06_30_173111_create_mfa_values_table.php b/database/migrations/2021_06_30_173111_create_mfa_values_table.php new file mode 100644 index 000000000..937fd31d9 --- /dev/null +++ b/database/migrations/2021_06_30_173111_create_mfa_values_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->integer('user_id')->index(); + $table->string('method', 20)->index(); + $table->text('value'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('mfa_values'); + } +} diff --git a/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php b/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php new file mode 100644 index 000000000..c14d47ea7 --- /dev/null +++ b/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php @@ -0,0 +1,32 @@ +boolean('mfa_enforced'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('mfa_enforced'); + }); + } +} diff --git a/database/migrations/2021_08_28_161743_add_export_role_permission.php b/database/migrations/2021_08_28_161743_add_export_role_permission.php new file mode 100644 index 000000000..1da607655 --- /dev/null +++ b/database/migrations/2021_08_28_161743_add_export_role_permission.php @@ -0,0 +1,49 @@ +get('id'); + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => 'content-export', + 'display_name' => 'Export Content', + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), + ]); + + $permissionRoles = $roles->map(function ($role) use ($permissionId) { + return [ + 'role_id' => $role->id, + 'permission_id' => $permissionId, + ]; + })->values()->toArray(); + + DB::table('permission_role')->insert($permissionRoles); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // Remove content-export permission + $contentExportPermission = DB::table('role_permissions') + ->where('name', '=', 'content-export')->first(); + + DB::table('permission_role')->where('permission_id', '=', $contentExportPermission->id)->delete(); + DB::table('role_permissions')->where('id', '=', 'content-export')->delete(); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index d86cb0ddd..069765eb4 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -1,7 +1,7 @@ $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id]; factory(\BookStack\Entities\Models\Book::class, 5)->create($byData) - ->each(function($book) use ($editorUser, $byData) { + ->each(function ($book) use ($byData) { $chapters = factory(Chapter::class, 3)->create($byData) - ->each(function($chapter) use ($editorUser, $book, $byData){ + ->each(function ($chapter) use ($book, $byData) { $pages = factory(Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id])); $chapter->pages()->saveMany($pages); }); @@ -58,11 +58,11 @@ class DummyContentSeeder extends Seeder $apiPermission = RolePermission::getByName('access-api'); $editorRole->attachPermission($apiPermission); $token = (new ApiToken())->forceFill([ - 'user_id' => $editorUser->id, - 'name' => 'Testing API key', + 'user_id' => $editorUser->id, + 'name' => 'Testing API key', 'expires_at' => ApiToken::defaultExpiry(), - 'secret' => Hash::make('password'), - 'token_id' => 'apitoken', + 'secret' => Hash::make('password'), + 'token_id' => 'apitoken', ]); $token->save(); diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile index 895ad595a..178ea9a6c 100644 --- a/dev/docker/Dockerfile +++ b/dev/docker/Dockerfile @@ -5,9 +5,9 @@ WORKDIR /app # Install additional dependacnies and configure apache RUN apt-get update -y \ - && apt-get install -y git zip unzip libpng-dev libldap2-dev wait-for-it \ + && apt-get install -y git zip unzip libpng-dev libldap2-dev libzip-dev wait-for-it \ && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \ - && docker-php-ext-install pdo_mysql gd ldap \ + && docker-php-ext-install pdo_mysql gd ldap zip \ && a2enmod rewrite \ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \ && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf @@ -20,4 +20,4 @@ RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ # Use the default production configuration and update it as required RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ - && sed -i 's/memory_limit = 128M/memory_limit = 512M/g' "$PHP_INI_DIR/php.ini" \ No newline at end of file + && sed -i 's/memory_limit = 128M/memory_limit = 512M/g' "$PHP_INI_DIR/php.ini" diff --git a/package-lock.json b/package-lock.json index 7d9318363..97d4d8287 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,19 +6,19 @@ "": { "dependencies": { "clipboard": "^2.0.8", - "codemirror": "^5.61.1", + "codemirror": "^5.62.3", "dropzone": "^5.9.2", - "markdown-it": "^12.0.6", + "markdown-it": "^12.2.0", "markdown-it-task-lists": "^2.1.1", - "sortablejs": "^1.13.0" + "sortablejs": "^1.14.0" }, "devDependencies": { - "chokidar-cli": "^2.1.0", - "esbuild": "0.12.8", + "chokidar-cli": "^3.0.0", + "esbuild": "0.12.22", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.34.1" + "sass": "^1.38.0" } }, "node_modules/ansi-regex": { @@ -43,9 +43,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -121,33 +121,33 @@ } }, "node_modules/chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "dependencies": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "glob-parent": "~5.1.0", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.6.0" }, "engines": { "node": ">= 8.10.0" }, "optionalDependencies": { - "fsevents": "~2.1.2" + "fsevents": "~2.3.2" } }, "node_modules/chokidar-cli": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-2.1.0.tgz", - "integrity": "sha512-6n21AVpW6ywuEPoxJcLXMA2p4T+SLjWsXKny/9yTWFz0kKxESI3eUylpeV97LylING/27T/RVTY0f2/0QaWq9Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz", + "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==", "dev": true, "dependencies": { - "chokidar": "^3.2.3", + "chokidar": "^3.5.2", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "yargs": "^13.3.0" @@ -181,9 +181,9 @@ } }, "node_modules/codemirror": { - "version": "5.61.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", - "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" + "version": "5.62.3", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz", + "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg==" }, "node_modules/color-convert": { "version": "1.9.3", @@ -313,9 +313,9 @@ } }, "node_modules/esbuild": { - "version": "0.12.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.8.tgz", - "integrity": "sha512-sx/LwlP/SWTGsd9G4RlOPrXnIihAJ2xwBUmzoqe2nWwbXORMQWtAGNJNYLBJJqa3e9PWvVzxdrtyFZJcr7D87g==", + "version": "0.12.22", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.22.tgz", + "integrity": "sha512-yWCr9RoFehpqoe/+MwZXJpYOEIt7KOEvNnjIeMZpMSyQt+KCBASM3y7yViiN5dJRphf1wGdUz1+M4rTtWd/ulA==", "dev": true, "hasInstallScript": true, "bin": { @@ -356,10 +356,11 @@ } }, "node_modules/fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -588,52 +589,6 @@ "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", "dev": true }, - "node_modules/livereload/node_modules/chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.1" - } - }, - "node_modules/livereload/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/livereload/node_modules/readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -675,9 +630,9 @@ "dev": true }, "node_modules/markdown-it": { - "version": "12.0.6", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz", - "integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz", + "integrity": "sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg==", "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", @@ -873,9 +828,9 @@ } }, "node_modules/path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "node_modules/path-type": { @@ -944,9 +899,9 @@ } }, "node_modules/readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "dependencies": { "picomatch": "^2.2.1" @@ -980,9 +935,9 @@ } }, "node_modules/sass": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz", - "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.38.0.tgz", + "integrity": "sha512-WBccZeMigAGKoI+NgD7Adh0ab1HUq+6BmyBUEaGxtErbUtWUevEbdgo5EZiJQofLUGcKtlNaO2IdN73AHEua5g==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0" @@ -1042,9 +997,9 @@ "dev": true }, "node_modules/sortablejs": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.13.0.tgz", - "integrity": "sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg==" + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" }, "node_modules/spdx-correct": { "version": "3.1.1", @@ -1295,9 +1250,9 @@ } }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -1358,28 +1313,28 @@ } }, "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.6.0" } }, "chokidar-cli": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-2.1.0.tgz", - "integrity": "sha512-6n21AVpW6ywuEPoxJcLXMA2p4T+SLjWsXKny/9yTWFz0kKxESI3eUylpeV97LylING/27T/RVTY0f2/0QaWq9Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz", + "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==", "dev": true, "requires": { - "chokidar": "^3.2.3", + "chokidar": "^3.5.2", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "yargs": "^13.3.0" @@ -1407,9 +1362,9 @@ } }, "codemirror": { - "version": "5.61.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz", - "integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==" + "version": "5.62.3", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz", + "integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg==" }, "color-convert": { "version": "1.9.3", @@ -1521,9 +1476,9 @@ } }, "esbuild": { - "version": "0.12.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.8.tgz", - "integrity": "sha512-sx/LwlP/SWTGsd9G4RlOPrXnIihAJ2xwBUmzoqe2nWwbXORMQWtAGNJNYLBJJqa3e9PWvVzxdrtyFZJcr7D87g==", + "version": "0.12.22", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.22.tgz", + "integrity": "sha512-yWCr9RoFehpqoe/+MwZXJpYOEIt7KOEvNnjIeMZpMSyQt+KCBASM3y7yViiN5dJRphf1wGdUz1+M4rTtWd/ulA==", "dev": true }, "escape-string-regexp": { @@ -1551,9 +1506,9 @@ } }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, @@ -1721,40 +1676,6 @@ "livereload-js": "^3.3.1", "opts": ">= 1.2.0", "ws": "^7.4.3" - }, - "dependencies": { - "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - } } }, "livereload-js": { @@ -1798,9 +1719,9 @@ "dev": true }, "markdown-it": { - "version": "12.0.6", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz", - "integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.2.0.tgz", + "integrity": "sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg==", "requires": { "argparse": "^2.0.1", "entities": "~2.1.0", @@ -1952,9 +1873,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-type": { @@ -2002,9 +1923,9 @@ } }, "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { "picomatch": "^2.2.1" @@ -2032,9 +1953,9 @@ } }, "sass": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz", - "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.38.0.tgz", + "integrity": "sha512-WBccZeMigAGKoI+NgD7Adh0ab1HUq+6BmyBUEaGxtErbUtWUevEbdgo5EZiJQofLUGcKtlNaO2IdN73AHEua5g==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0" @@ -2079,9 +2000,9 @@ "dev": true }, "sortablejs": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.13.0.tgz", - "integrity": "sha512-RBJirPY0spWCrU5yCmWM1eFs/XgX2J5c6b275/YyxFRgnzPhKl/TDeU2hNR8Dt7ITq66NRPM4UlOt+e5O4CFHg==" + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" }, "spdx-correct": { "version": "3.1.1", diff --git a/package.json b/package.json index f4b7bf5dc..d2740cd81 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,19 @@ "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads" }, "devDependencies": { - "chokidar-cli": "^2.1.0", - "esbuild": "0.12.8", + "chokidar-cli": "^3.0.0", + "esbuild": "0.12.22", "livereload": "^0.9.3", "npm-run-all": "^4.1.5", "punycode": "^2.1.1", - "sass": "^1.34.1" + "sass": "^1.38.0" }, "dependencies": { "clipboard": "^2.0.8", - "codemirror": "^5.61.1", + "codemirror": "^5.62.3", "dropzone": "^5.9.2", - "markdown-it": "^12.0.6", + "markdown-it": "^12.2.0", "markdown-it-task-lists": "^2.1.1", - "sortablejs": "^1.13.0" + "sortablejs": "^1.14.0" } } diff --git a/phpcs.xml b/phpcs.xml deleted file mode 100644 index 8d5157d9e..000000000 --- a/phpcs.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - The coding standard for BookStack. - - ./app - */migrations/* - */tests/* - - - - \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 75c89ec33..7e0da05d4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -37,6 +37,7 @@ + diff --git a/public/index.php b/public/index.php index 9d890e90a..7e4ef97c7 100644 --- a/public/index.php +++ b/public/index.php @@ -1,12 +1,10 @@ */ - define('LARAVEL_START', microtime(true)); /* @@ -21,7 +19,7 @@ define('LARAVEL_START', microtime(true)); | */ -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- @@ -35,7 +33,7 @@ require __DIR__.'/../vendor/autoload.php'; | */ -$app = require_once __DIR__.'/../bootstrap/app.php'; +$app = require_once __DIR__ . '/../bootstrap/app.php'; $app->alias('request', \BookStack\Http\Request::class); /* @@ -58,4 +56,4 @@ $response = $kernel->handle( $response->send(); -$kernel->terminate($request, $response); \ No newline at end of file +$kernel->terminate($request, $response); diff --git a/readme.md b/readme.md index eb98ae6d4..cb17a1aae 100644 --- a/readme.md +++ b/readme.md @@ -3,9 +3,10 @@ [![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE) [![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack) -[![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions) [![Discord](https://img.shields.io/static/v1?label=chat&message=discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2) [![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/) +[![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions) +[![StyleCI](https://github.styleci.io/repos/41589337/shield?style=flat)](https://github.styleci.io/repos/41589337) A platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/. @@ -81,7 +82,8 @@ Once done you can run `php vendor/bin/phpunit` in the application root directory ### 📜 Code 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. Please don't auto-fix code unless it's related to changes you've made otherwise you'll likely cause git conflicts. +PHP code style is enforced automatically [using StyleCI](https://github.styleci.io/repos/41589337). +If submitting a PR, any formatting changes to be made will be automatically fixed after merging. ### 🐋 Development using Docker @@ -188,3 +190,6 @@ These are the great open-source projects used to help build BookStack: * [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml) * [League/CommonMark](https://commonmark.thephpleague.com/) * [League/Flysystem](https://flysystem.thephpleague.com) +* [StyleCI](https://styleci.io/) +* [pragmarx/google2fa](https://github.com/antonioribeiro/google2fa) +* [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode) \ No newline at end of file diff --git a/resources/lang/ar/activities.php b/resources/lang/ar/activities.php index 05055d1bc..18ca16571 100644 --- a/resources/lang/ar/activities.php +++ b/resources/lang/ar/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'تم التعليق', 'permissions_update' => 'تحديث الأذونات', diff --git a/resources/lang/ar/auth.php b/resources/lang/ar/auth.php index 363310bba..c0cc8bbc0 100644 --- a/resources/lang/ar/auth.php +++ b/resources/lang/ar/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'مرحبا بكم في :appName!', 'user_invite_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة مرور سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.', 'user_invite_page_confirm_button' => 'تأكيد كلمة المرور', - 'user_invite_success' => 'مجموعة كلمات المرور، لديك الآن حق الوصول إلى :appName!' + 'user_invite_success' => 'مجموعة كلمات المرور، لديك الآن حق الوصول إلى :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/ar/common.php b/resources/lang/ar/common.php index b9b203234..de40e583e 100644 --- a/resources/lang/ar/common.php +++ b/resources/lang/ar/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'إعادة تعيين', 'remove' => 'إزالة', 'add' => 'إضافة', + 'configure' => 'Configure', 'fullscreen' => 'شاشة كاملة', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'لا يوجد نشاط لعرضه', 'no_items' => 'لا توجد عناصر متوفرة', 'back_to_top' => 'العودة إلى الأعلى', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'عرض / إخفاء التفاصيل', 'toggle_thumbnails' => 'عرض / إخفاء الصور المصغرة', 'details' => 'التفاصيل', diff --git a/resources/lang/ar/entities.php b/resources/lang/ar/entities.php index dec4522f4..7e6dfc7a1 100644 --- a/resources/lang/ar/entities.php +++ b/resources/lang/ar/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'صفحة ويب', 'export_pdf' => 'ملف PDF', 'export_text' => 'ملف نص عادي', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'الأذونات', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'أذونات رف الكتب', 'shelves_permissions_updated' => 'تم تحديث أذونات رف الكتب', 'shelves_permissions_active' => 'أذونات رف الكتب نشطة', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب', 'shelves_copy_permissions' => 'نسخ الأذونات', 'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الأذونات الحالية لهذا الرف على جميع الكتب المتضمنة فيه. قبل التفعيل، تأكد من حفظ أي تغييرات في أذونات هذا الرف.', diff --git a/resources/lang/ar/settings.php b/resources/lang/ar/settings.php index 88f68a2f8..05cf9e235 100755 --- a/resources/lang/ar/settings.php +++ b/resources/lang/ar/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'سلة المحذوفات', 'recycle_bin_desc' => 'هنا يمكنك استعادة العناصر التي تم حذفها أو اختيار إزالتها نهائيا من النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.', 'recycle_bin_deleted_item' => 'عنصر محذوف', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'حُذف بواسطة', 'recycle_bin_deleted_at' => 'وقت الحذف', 'recycle_bin_permanently_delete' => 'حُذف نهائيًا', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'العناصر المراد استرجاعها', 'recycle_bin_restore_confirm' => 'سيعيد هذا الإجراء العنصر المحذوف ، بما في ذلك أي عناصر فرعية ، إلى موقعه الأصلي. إذا تم حذف الموقع الأصلي منذ ذلك الحين ، وهو الآن في سلة المحذوفات ، فسيلزم أيضًا استعادة العنصر الأصلي.', 'recycle_bin_restore_deleted_parent' => 'تم حذف أصل هذا العنصر أيضًا. سيبقى حذفه حتى يتم استعادة ذلك الأصل أيضًا.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'المحذوف: قُم بعد إجمالي العناصر من سلة المحذوفات.', 'recycle_bin_restore_notification' => 'المرتجع: قُم بعد إجمالي العناصر من سلة المحذوفات.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'تفاصيل الدور', 'role_name' => 'اسم الدور', 'role_desc' => 'وصف مختصر للدور', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'ربط الحساب بمواقع التواصل', 'role_system' => 'أذونات النظام', 'role_manage_users' => 'إدارة المستخدمين', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'إدارة قوالب الصفحة', 'role_access_api' => 'الوصول إلى واجهة برمجة تطبيقات النظام API', 'role_manage_settings' => 'إدارة إعدادات التطبيق', + 'role_export_content' => 'Export content', 'role_asset' => 'أذونات الأصول', 'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.', 'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'قم بإنشاء رمز مميز', 'users_api_tokens_expires' => 'انتهاء مدة الصلاحية', 'users_api_tokens_docs' => 'وثائق API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'قم بإنشاء رمز API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/ar/validation.php b/resources/lang/ar/validation.php index 3c03f2efd..d4d3aaf26 100644 --- a/resources/lang/ar/validation.php +++ b/resources/lang/ar/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'يجب أن يقتصر :attribute على حروف أو أرقام أو شرطات فقط.', 'alpha_num' => 'يجب أن يقتصر :attribute على الحروف والأرقام فقط.', 'array' => 'يجب أن تكون السمة مصفوفة.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'يجب أن يكون التاريخ :attribute قبل :date.', 'between' => [ 'numeric' => 'يجب أن يكون :attribute بين :min و :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'يجب أن تكون السمة: سلسلة.', 'timezone' => 'يجب أن تكون :attribute منطقة صالحة.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'تم حجز :attribute من قبل.', 'url' => 'صيغة :attribute غير صالحة.', 'uploaded' => 'تعذر تحميل الملف. قد لا يقبل الخادم ملفات بهذا الحجم.', diff --git a/resources/lang/bg/activities.php b/resources/lang/bg/activities.php index 90218a30a..15715baf9 100644 --- a/resources/lang/bg/activities.php +++ b/resources/lang/bg/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'коментирано на', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/bg/auth.php b/resources/lang/bg/auth.php index 018e871e1..d04796cfe 100644 --- a/resources/lang/bg/auth.php +++ b/resources/lang/bg/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Добре дошли в :appName!', 'user_invite_page_text' => 'За да финализирате вашият акаунт и да получите достъп трябва да определите парола, която да бъде използвана за следващия влизания в :appName.', 'user_invite_page_confirm_button' => 'Потвърди паролата', - 'user_invite_success' => 'Паролата е потвърдена и вече имате достъп до :appName!' + 'user_invite_success' => 'Паролата е потвърдена и вече имате достъп до :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/bg/common.php b/resources/lang/bg/common.php index 7c833bd0d..4b0f72a60 100644 --- a/resources/lang/bg/common.php +++ b/resources/lang/bg/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Нулирай', 'remove' => 'Премахване', 'add' => 'Добави', + 'configure' => 'Configure', 'fullscreen' => 'Пълен екран', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Няма активност за показване', 'no_items' => 'Няма налични артикули', 'back_to_top' => 'Върнете се в началото', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Активирай детайли', 'toggle_thumbnails' => 'Активирай миниатюри', 'details' => 'Подробности', diff --git a/resources/lang/bg/entities.php b/resources/lang/bg/entities.php index accf2157a..4880ad25d 100644 --- a/resources/lang/bg/entities.php +++ b/resources/lang/bg/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Прикачени уеб файлове', 'export_pdf' => 'PDF файл', 'export_text' => 'Обикновен текстов файл', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Права', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Настройки за достъп до рафта с книги', 'shelves_permissions_updated' => 'Настройките за достъп до рафта с книги е обновен', 'shelves_permissions_active' => 'Настройките за достъп до рафта с книги е активен', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Копирай настойките за достъп към книгите', 'shelves_copy_permissions' => 'Копирай настройките за достъп', 'shelves_copy_permissions_explain' => 'Това ще приложи настоящите настройки за достъп на този рафт с книги за всички книги, съдържащи се в него. Преди да активирате, уверете се, че всички промени в настройките за достъп на този рафт са запазени.', diff --git a/resources/lang/bg/settings.php b/resources/lang/bg/settings.php index c32d3a5dd..0cf676851 100644 --- a/resources/lang/bg/settings.php +++ b/resources/lang/bg/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Кошче', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Изтрит предмет', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Изтрит от', 'recycle_bin_deleted_at' => 'Час на изтриване', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Детайли на роля', 'role_name' => 'Име на ролята', 'role_desc' => 'Кратко описание на ролята', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Външни ауторизиращи ID-a', 'role_system' => 'Настойки за достъп на системата', 'role_manage_users' => 'Управление на потребители', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Управление на шаблони на страници', 'role_access_api' => 'Достъп до API на системата', 'role_manage_settings' => 'Управление на настройките на приложението', + 'role_export_content' => 'Export content', 'role_asset' => 'Настройки за достъп до активи', 'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.', 'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Create Token', 'users_api_tokens_expires' => 'Expires', 'users_api_tokens_docs' => 'API Documentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/bg/validation.php b/resources/lang/bg/validation.php index 3f22751dd..0a5b81d92 100644 --- a/resources/lang/bg/validation.php +++ b/resources/lang/bg/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute може да съдържа само букви, числа, тире и долна черта.', 'alpha_num' => ':attribute може да съдържа само букви и числа.', 'array' => ':attribute трябва да е масив (array).', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute трябва да е дата след :date.', 'between' => [ 'numeric' => ':attribute трябва да е между :min и :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'The :attribute must be a string.', 'timezone' => 'The :attribute must be a valid zone.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'The :attribute has already been taken.', 'url' => 'The :attribute format is invalid.', 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', diff --git a/resources/lang/bs/activities.php b/resources/lang/bs/activities.php index c4bb5a27e..9136e651b 100644 --- a/resources/lang/bs/activities.php +++ b/resources/lang/bs/activities.php @@ -44,8 +44,12 @@ return [ 'bookshelf_delete_notification' => 'Polica za knjige Uspješno Izbrisana', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" je dodan u tvoje favorite', + 'favourite_remove_notification' => '":name" je uklonjen iz tvojih favorita', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other 'commented_on' => 'je komentarisao/la na', diff --git a/resources/lang/bs/auth.php b/resources/lang/bs/auth.php index 526a8612f..a5926fa2b 100644 --- a/resources/lang/bs/auth.php +++ b/resources/lang/bs/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Dobrodošli na :appName!', 'user_invite_page_text' => 'Da biste završili vaš račun i dobili pristup morate postaviti lozinku koju ćete koristiti da se prijavite na :appName tokom budućih posjeta.', 'user_invite_page_confirm_button' => 'Potvrdi lozinku', - 'user_invite_success' => 'Lozinka postavljena, sada imate pristup :sppName!' + 'user_invite_success' => 'Lozinka postavljena, sada imate pristup :sppName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/bs/common.php b/resources/lang/bs/common.php index 4a1af9de0..17ec0b765 100644 --- a/resources/lang/bs/common.php +++ b/resources/lang/bs/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Resetuj', 'remove' => 'Ukloni', 'add' => 'Dodaj', + 'configure' => 'Configure', 'fullscreen' => 'Prikaz preko čitavog ekrana', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Favorit', + 'unfavourite' => 'Ukloni favorit', + 'next' => 'Sljedeće', + 'previous' => 'Prethodno', // Sort Options 'sort_options' => 'Opcije sortiranja', @@ -51,7 +52,7 @@ return [ 'sort_ascending' => 'Sortiraj uzlazno', 'sort_descending' => 'Sortiraj silazno', 'sort_name' => 'Ime', - 'sort_default' => 'Default', + 'sort_default' => 'Početne postavke', 'sort_created_at' => 'Datum kreiranja', 'sort_updated_at' => 'Datum ažuriranja', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nema aktivnosti za prikazivanje', 'no_items' => 'Nema dostupnih stavki', 'back_to_top' => 'Povratak na vrh', + 'skip_to_main_content' => 'Idi odmah na glavni sadržaj', 'toggle_details' => 'Vidi detalje', 'toggle_thumbnails' => 'Vidi prikaze slika', 'details' => 'Detalji', @@ -69,7 +71,7 @@ return [ 'breadcrumb' => 'Navigacijske stavke', // Header - 'header_menu_expand' => 'Expand Header Menu', + 'header_menu_expand' => 'Otvori meni u zaglavlju', 'profile_menu' => 'Meni profila', 'view_profile' => 'Pogledaj profil', 'edit_profile' => 'Izmjeni profil', @@ -78,9 +80,9 @@ return [ // Layout tabs 'tab_info' => 'Informacije', - 'tab_info_label' => 'Tab: Show Secondary Information', + 'tab_info_label' => 'Kartica: Prikaži dodatnu informaciju', 'tab_content' => 'Sadržaj', - 'tab_content_label' => 'Tab: Show Primary Content', + 'tab_content_label' => 'Kartica: Prikaži glavni sadržaj', // Email Content 'email_action_help' => 'Ukoliko imate poteškoća sa pritiskom na ":actionText" dugme, kopirajte i zaljepite URL koji se nalazi ispod u vaš web pretraživač:', diff --git a/resources/lang/bs/entities.php b/resources/lang/bs/entities.php index 14f4e351c..52924d96f 100644 --- a/resources/lang/bs/entities.php +++ b/resources/lang/bs/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Sadržani web fajl', 'export_pdf' => 'PDF fajl', 'export_text' => 'Plain Text fajl', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Dozvole', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Bookshelf Permissions', 'shelves_permissions_updated' => 'Bookshelf Permissions Updated', 'shelves_permissions_active' => 'Bookshelf Permissions Active', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books', 'shelves_copy_permissions' => 'Copy Permissions', 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.', diff --git a/resources/lang/bs/settings.php b/resources/lang/bs/settings.php index 8a7946b12..4c1ae1345 100644 --- a/resources/lang/bs/settings.php +++ b/resources/lang/bs/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Role Details', 'role_name' => 'Role Name', 'role_desc' => 'Short Description of Role', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'System Permissions', 'role_manage_users' => 'Manage users', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Manage page templates', 'role_access_api' => 'Access system API', 'role_manage_settings' => 'Manage app settings', + 'role_export_content' => 'Export content', 'role_asset' => 'Asset Permissions', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Create Token', 'users_api_tokens_expires' => 'Expires', 'users_api_tokens_docs' => 'API Documentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/bs/validation.php b/resources/lang/bs/validation.php index f3af075fb..d6887ccc7 100644 --- a/resources/lang/bs/validation.php +++ b/resources/lang/bs/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute može sadržavati samo slova, brojeve, crtice i donje crtice.', 'alpha_num' => ':attribute može sadržavati samo slova i brojeve.', 'array' => ':attribute mora biti niz.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute mora biti datum prije :date.', 'between' => [ 'numeric' => ':attribute mora biti između :min i :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute mora biti string.', 'timezone' => ':attribute mora biti ispravna zona.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute je zauzet.', 'url' => 'Format :attribute je neispravan.', 'uploaded' => 'Fajl nije učitan. Server ne prihvata fajlove ove veličine.', diff --git a/resources/lang/ca/activities.php b/resources/lang/ca/activities.php index 604eaeda4..18878c2a6 100644 --- a/resources/lang/ca/activities.php +++ b/resources/lang/ca/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'ha comentat a', 'permissions_update' => 'ha actualitzat els permisos', diff --git a/resources/lang/ca/auth.php b/resources/lang/ca/auth.php index a66f75f94..9febe36b7 100644 --- a/resources/lang/ca/auth.php +++ b/resources/lang/ca/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Us donem la benvinguda a :appName!', 'user_invite_page_text' => 'Per a enllestir el vostre compte i obtenir-hi accés, cal que definiu una contrasenya, que es farà servir per a iniciar la sessió a :appName en futures visites.', 'user_invite_page_confirm_button' => 'Confirma la contrasenya', - 'user_invite_success' => 'S\'ha establert la contrasenya, ara ja teniu accés a :appName!' + 'user_invite_success' => 'S\'ha establert la contrasenya, ara ja teniu accés a :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/ca/common.php b/resources/lang/ca/common.php index 03d38a2d3..497395060 100644 --- a/resources/lang/ca/common.php +++ b/resources/lang/ca/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Reinicialitza', 'remove' => 'Elimina', 'add' => 'Afegeix', + 'configure' => 'Configure', 'fullscreen' => 'Pantalla completa', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'No hi ha activitat', 'no_items' => 'No hi ha cap element', 'back_to_top' => 'Torna a dalt', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Commuta els detalls', 'toggle_thumbnails' => 'Commuta les miniatures', 'details' => 'Detalls', diff --git a/resources/lang/ca/entities.php b/resources/lang/ca/entities.php index 55921f3b6..d3abdeae7 100644 --- a/resources/lang/ca/entities.php +++ b/resources/lang/ca/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Fitxer web independent', 'export_pdf' => 'Fitxer PDF', 'export_text' => 'Fitxer de text sense format', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Permisos', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permisos del prestatge', 'shelves_permissions_updated' => 'S\'han actualitzat els permisos del prestatge', 'shelves_permissions_active' => 'S\'han activat els permisos del prestatge', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copia els permisos als llibres', 'shelves_copy_permissions' => 'Copia els permisos', 'shelves_copy_permissions_explain' => 'Això aplicarà la configuració de permisos actual d\'aquest prestatge a tots els llibres que contingui. Abans d\'activar-ho, assegureu-vos que hàgiu desat qualsevol canvi als permisos d\'aquest prestatge.', diff --git a/resources/lang/ca/settings.php b/resources/lang/ca/settings.php index 165c014b4..e6a6648d2 100755 --- a/resources/lang/ca/settings.php +++ b/resources/lang/ca/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Paperera de reciclatge', 'recycle_bin_desc' => 'Aquí podeu restaurar els elements que hàgiu suprimit o triar suprimir-los del sistema de manera permanent. Aquesta llista no té cap filtre, al contrari que altres llistes d\'activitat similars en què es tenen en compte els filtres de permisos.', 'recycle_bin_deleted_item' => 'Element suprimit', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Suprimit per', 'recycle_bin_deleted_at' => 'Moment de la supressió', 'recycle_bin_permanently_delete' => 'Suprimeix permanentment', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Elements que es restauraran', 'recycle_bin_restore_confirm' => 'Aquesta acció restaurarà l\'element suprimit, incloent-hi tots els elements fills, a la seva ubicació original. Si la ubicació original ha estat suprimida, i ara és a la paperera de reciclatge, caldrà que també en restaureu l\'element pare.', 'recycle_bin_restore_deleted_parent' => 'El pare d\'aquest element també ha estat suprimit. L\'element es mantindrà suprimit fins que el pare també es restauri.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'S\'han suprimit :count elements en total de la paperera de reciclatge.', 'recycle_bin_restore_notification' => 'S\'han restaurat :count elements en total de la paperera de reciclatge.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detalls del rol', 'role_name' => 'Nom del rol', 'role_desc' => 'Descripció curta del rol', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Identificadors d\'autenticació externa', 'role_system' => 'Permisos del sistema', 'role_manage_users' => 'Gestiona usuaris', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Gestiona les plantilles de pàgines', 'role_access_api' => 'Accedeix a l\'API del sistema', 'role_manage_settings' => 'Gestiona la configuració de l\'aplicació', + 'role_export_content' => 'Export content', 'role_asset' => 'Permisos de recursos', 'roles_system_warning' => 'Tingueu en compte que l\'accés a qualsevol dels tres permisos de dalt pot permetre que un usuari alteri els seus propis permisos o els privilegis d\'altres usuaris del sistema. Assigneu rols amb aquests permisos només a usuaris de confiança.', 'role_asset_desc' => 'Aquests permisos controlen l\'accés per defecte als recursos del sistema. Els permisos de llibres, capítols i pàgines tindran més importància que aquests permisos.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Crea un testimoni', 'users_api_tokens_expires' => 'Caducitat', 'users_api_tokens_docs' => 'Documentació de l\'API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Crea un testimoni d\'API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/ca/validation.php b/resources/lang/ca/validation.php index c134c36c6..603182c1a 100644 --- a/resources/lang/ca/validation.php +++ b/resources/lang/ca/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'El camp :attribute només pot contenir lletres, números, guions i guions baixos.', 'alpha_num' => 'El camp :attribute només pot contenir lletres i números.', 'array' => 'El camp :attribute ha de ser un vector.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'El camp :attribute ha de ser una data anterior a :date.', 'between' => [ 'numeric' => 'El camp :attribute ha d\'estar entre :min i :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'El camp :attribute ha de ser una cadena.', 'timezone' => 'El camp :attribute ha de ser una zona vàlida.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'El camp :attribute ja està ocupat.', 'url' => 'El format del camp :attribute no és vàlid.', 'uploaded' => 'No s\'ha pogut pujar el fitxer. És possible que el servidor no accepti fitxers d\'aquesta mida.', diff --git a/resources/lang/cs/activities.php b/resources/lang/cs/activities.php index e444399cc..717814702 100644 --- a/resources/lang/cs/activities.php +++ b/resources/lang/cs/activities.php @@ -11,7 +11,7 @@ return [ 'page_update' => 'aktualizoval/a stránku', 'page_update_notification' => 'Stránka byla úspěšně aktualizována', 'page_delete' => 'odstranil/a stránku', - 'page_delete_notification' => 'Stránka byla úspěšně smazána', + 'page_delete_notification' => 'Stránka byla odstraněna', 'page_restore' => 'obnovil/a stránku', 'page_restore_notification' => 'Stránka byla úspěšně obnovena', 'page_move' => 'přesunul/a stránku', @@ -21,19 +21,19 @@ return [ 'chapter_create_notification' => 'Kapitola byla úspěšně vytvořena', 'chapter_update' => 'aktualizoval/a kapitolu', 'chapter_update_notification' => 'Kapitola byla úspěšně aktualizována', - 'chapter_delete' => 'smazal/a kapitolu', - 'chapter_delete_notification' => 'Kapitola byla úspěšně smazána', + 'chapter_delete' => 'odstranila/a kapitolu', + 'chapter_delete_notification' => 'Kapitola byla odstraněna', 'chapter_move' => 'přesunul/a kapitolu', // Books 'book_create' => 'vytvořil/a knihu', - 'book_create_notification' => 'Kniha byla úspěšně vytvořena', + 'book_create_notification' => 'Kniha byla vytvořena', 'book_update' => 'aktualizoval/a knihu', - 'book_update_notification' => 'Kniha byla úspěšně aktualizována', - 'book_delete' => 'smazal/a knihu', - 'book_delete_notification' => 'Kniha byla úspěšně smazána', + 'book_update_notification' => 'Kniha byla aktualizována', + 'book_delete' => 'odstranil/a knihu', + 'book_delete_notification' => 'Kniha byla odstraněna', 'book_sort' => 'seřadil/a knihu', - 'book_sort_notification' => 'Kniha byla úspěšně seřazena', + 'book_sort_notification' => 'Kniha byla seřazena', // Bookshelves 'bookshelf_create' => 'vytvořil/a knihovnu', @@ -41,13 +41,17 @@ return [ 'bookshelf_update' => 'aktualizoval/a knihovnu', 'bookshelf_update_notification' => 'Knihovna byla úspěšně aktualizována', 'bookshelf_delete' => 'odstranil/a knihovnu', - 'bookshelf_delete_notification' => 'Knihovna byla úspěšně odstraněna', + 'bookshelf_delete_notification' => 'Knihovna byla odstraněna', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" byla přidána do Vašich oblíbených', + 'favourite_remove_notification' => '":name" byla odstraněna z Vašich oblíbených', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other 'commented_on' => 'okomentoval/a', - 'permissions_update' => 'updated permissions', + 'permissions_update' => 'oprávnění upravena', ]; diff --git a/resources/lang/cs/auth.php b/resources/lang/cs/auth.php index a9f36c390..f8cdb7747 100644 --- a/resources/lang/cs/auth.php +++ b/resources/lang/cs/auth.php @@ -6,45 +6,45 @@ */ return [ - 'failed' => 'Tyto přihlašovací údaje neodpovídají našim záznamům.', + 'failed' => 'Neplatné přihlašovací údaje.', 'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.', // Login & Register 'sign_up' => 'Registrace', 'log_in' => 'Přihlášení', - 'log_in_with' => 'Přihlásit se pomocí :socialDriver', - 'sign_up_with' => 'Registrovat se pomocí :socialDriver', + 'log_in_with' => 'Přihlásit se přes :socialDriver', + 'sign_up_with' => 'Registrovat se přes :socialDriver', 'logout' => 'Odhlásit', 'name' => 'Jméno', 'username' => 'Uživatelské jméno', 'email' => 'E-mail', 'password' => 'Heslo', - 'password_confirm' => 'Potvrdit heslo', - 'password_hint' => 'Musí mít více než 7 znaků', - 'forgot_password' => 'Zapomněli jste heslo?', + 'password_confirm' => 'Potvrzení hesla', + 'password_hint' => 'Musí mít víc než 7 znaků', + 'forgot_password' => 'Zapomenuté heslo?', 'remember_me' => 'Zapamatovat si mě', 'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.', 'create_account' => 'Vytvořit účet', 'already_have_account' => 'Již máte účet?', - 'dont_have_account' => 'Nemáte účet?', - 'social_login' => 'Přihlášení pomocí sociálních sítí', - 'social_registration' => 'Přihlášení pomocí sociálních sítí', - 'social_registration_text' => 'Registrovat a přihlásit se pomocí jiné služby.', + 'dont_have_account' => 'Nemáte učet?', + 'social_login' => 'Přihlášení přes sociální sítě', + 'social_registration' => 'Registrace přes sociální sítě', + 'social_registration_text' => 'Registrovat a přihlásit se přes jinou službu', 'register_thanks' => 'Děkujeme za registraci!', 'register_confirm' => 'Zkontrolujte prosím svůj e-mail a klikněte na potvrzovací tlačítko pro přístup do :appName.', - 'registrations_disabled' => 'Registrace jsou aktuálně zakázány', - 'registration_email_domain_invalid' => 'Tato e-mailová doména nemá přístup k této aplikaci', + 'registrations_disabled' => 'Registrace jsou momentálně pozastaveny', + 'registration_email_domain_invalid' => 'Registrace z této e-mailové domény nejsou povoleny', 'register_success' => 'Děkujeme za registraci! Nyní jste zaregistrováni a přihlášeni.', // Password Reset 'reset_password' => 'Obnovit heslo', - 'reset_password_send_instructions' => 'Níže zadejte svou e-mailovou adresu a bude vám zaslán e-mail s odkazem pro obnovení hesla.', - 'reset_password_send_button' => 'Zaslat odkaz pro obnovení', + 'reset_password_send_instructions' => 'Níže zadejte svou e-mailovou adresu a bude vám zaslán e-mail s odkazem na obnovení hesla.', + 'reset_password_send_button' => 'Zaslat odkaz na obnovení hesla', 'reset_password_sent' => 'Odkaz pro obnovení hesla bude odeslán na :email, pokud bude tato e-mailová adresa nalezena v systému.', - 'reset_password_success' => 'Vaše heslo bylo úspěšně obnoveno.', + 'reset_password_success' => 'Vaše heslo bylo obnoveno.', 'email_reset_subject' => 'Obnovit heslo do :appName', 'email_reset_text' => 'Tento e-mail jste obdrželi, protože jsme obdrželi žádost o obnovení hesla k vašemu účtu.', 'email_reset_not_requested' => 'Pokud jste o obnovení hesla nežádali, není vyžadována žádná další akce.', @@ -66,12 +66,47 @@ return [ 'email_not_confirmed_resend_button' => 'Znovu odeslat potvrzovací e-mail', // User Invite - 'user_invite_email_subject' => 'Byli jste pozváni přidat se do :appName!', + 'user_invite_email_subject' => 'Byli jste pozváni do :appName!', 'user_invite_email_greeting' => 'Byl pro vás vytvořen účet na :appName.', 'user_invite_email_text' => 'Klikněte na níže uvedené tlačítko pro nastavení hesla k účtu a získání přístupu:', 'user_invite_email_action' => 'Nastavit heslo k účtu', 'user_invite_page_welcome' => 'Vítejte v :appName!', - 'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při budoucích návštěvách.', + 'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při dalších návštěvách.', 'user_invite_page_confirm_button' => 'Potvrdit heslo', - 'user_invite_success' => 'Heslo nastaveno, nyní máte přístup k :appName!' + 'user_invite_success' => 'Heslo nastaveno, nyní máte přístup k :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/cs/common.php b/resources/lang/cs/common.php index b76fa32e4..88821ecf9 100644 --- a/resources/lang/cs/common.php +++ b/resources/lang/cs/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Obnovit', 'remove' => 'Odebrat', 'add' => 'Přidat', + 'configure' => 'Configure', 'fullscreen' => 'Celá obrazovka', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Přidat do oblíbených', + 'unfavourite' => 'Odebrat z oblíbených', + 'next' => 'Další', + 'previous' => 'Předchozí', // Sort Options 'sort_options' => 'Možnosti řazení', @@ -51,7 +52,7 @@ return [ 'sort_ascending' => 'Řadit vzestupně', 'sort_descending' => 'Řadit sestupně', 'sort_name' => 'Název', - 'sort_default' => 'Default', + 'sort_default' => 'Výchozí', 'sort_created_at' => 'Datum vytvoření', 'sort_updated_at' => 'Datum aktualizace', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Žádná aktivita k zobrazení', 'no_items' => 'Žádné položky k dispozici', 'back_to_top' => 'Zpět na začátek', + 'skip_to_main_content' => 'Přeskočit na hlavní obsah', 'toggle_details' => 'Přepnout podrobnosti', 'toggle_thumbnails' => 'Přepnout náhledy', 'details' => 'Podrobnosti', @@ -69,7 +71,7 @@ return [ 'breadcrumb' => 'Drobečková navigace', // Header - 'header_menu_expand' => 'Expand Header Menu', + 'header_menu_expand' => 'Rozbalit menu v záhlaví', 'profile_menu' => 'Nabídka profilu', 'view_profile' => 'Zobrazit profil', 'edit_profile' => 'Upravit profil', @@ -78,9 +80,9 @@ return [ // Layout tabs 'tab_info' => 'Informace', - 'tab_info_label' => 'Tab: Show Secondary Information', + 'tab_info_label' => 'Tab: Zobrazit podružné informace', 'tab_content' => 'Obsah', - 'tab_content_label' => 'Tab: Show Primary Content', + 'tab_content_label' => 'Tab: Zobrazit hlavní obsah', // Email Content 'email_action_help' => 'Pokud se vám nedaří kliknout na tlačítko „:actionText“, zkopírujte a vložte níže uvedenou URL do vašeho webového prohlížeče:', @@ -88,6 +90,6 @@ return [ // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Privacy Policy', - 'terms_of_service' => 'Terms of Service', + 'privacy_policy' => 'Zásady ochrany osobních údajů', + 'terms_of_service' => 'Podmínky služby', ]; diff --git a/resources/lang/cs/components.php b/resources/lang/cs/components.php index e06462b00..20df62e36 100644 --- a/resources/lang/cs/components.php +++ b/resources/lang/cs/components.php @@ -16,13 +16,13 @@ return [ 'image_image_name' => 'Název obrázku', 'image_delete_used' => 'Tento obrázek je použit na níže uvedených stránkách.', 'image_delete_confirm_text' => 'Opravdu chcete odstranit tento obrázek?', - 'image_select_image' => 'Vyberte obrázek', + 'image_select_image' => 'Zvolte obrázek', 'image_dropzone' => 'Přetáhněte obrázky nebo klikněte sem pro nahrání', 'images_deleted' => 'Obrázky odstraněny', 'image_preview' => 'Náhled obrázku', - 'image_upload_success' => 'Obrázek byl úspěšně nahrán', - 'image_update_success' => 'Podrobnosti o obrázku byly úspěšně aktualizovány', - 'image_delete_success' => 'Obrázek byl úspěšně odstraněn', + 'image_upload_success' => 'Obrázek byl nahrán', + 'image_update_success' => 'Podrobnosti o obrázku byly aktualizovány', + 'image_delete_success' => 'Obrázek byl odstraněn', 'image_upload_remove' => 'Odebrat', // Code Editor diff --git a/resources/lang/cs/entities.php b/resources/lang/cs/entities.php index 1a843c93d..97823a708 100644 --- a/resources/lang/cs/entities.php +++ b/resources/lang/cs/entities.php @@ -15,38 +15,39 @@ return [ 'recently_update' => 'Nedávno aktualizované', 'recently_viewed' => 'Nedávno zobrazené', 'recent_activity' => 'Nedávné aktivity', - 'create_now' => 'Vytvořte ji nyní', + 'create_now' => 'Vytvořit nyní', 'revisions' => 'Revize', 'meta_revision' => 'Revize č. :revisionCount', 'meta_created' => 'Vytvořeno :timeLength', 'meta_created_name' => 'Vytvořeno :timeLength uživatelem :user', 'meta_updated' => 'Aktualizováno :timeLength', 'meta_updated_name' => 'Aktualizováno :timeLength uživatelem :user', - 'meta_owned_name' => 'Owned by :user', + 'meta_owned_name' => 'Vlastník :user', 'entity_select' => 'Výběr entity', 'images' => 'Obrázky', 'my_recent_drafts' => 'Mé nedávné koncepty', 'my_recently_viewed' => 'Mé nedávno zobrazené', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => 'Mé nejčastěji zobrazené oblíbené', + 'my_favourites' => 'Mé oblíbené', 'no_pages_viewed' => 'Nezobrazili jste žádné stránky', - 'no_pages_recently_created' => 'Nedávno nebyly vytvořeny žádné stránky', - 'no_pages_recently_updated' => 'Nedávno nebyly aktualizovány žádné stránky', + 'no_pages_recently_created' => 'Žádné nedávno vytvořené stránky', + 'no_pages_recently_updated' => 'Žádné nedávno aktualizované stránky', 'export' => 'Exportovat', - 'export_html' => 'Konsolidovaný webový soubor', - 'export_pdf' => 'Soubor PDF', + 'export_html' => 'HTML stránka s celým obsahem', + 'export_pdf' => 'PDF dokument', 'export_text' => 'Textový soubor', + 'export_md' => 'Markdown', // Permissions and restrictions 'permissions' => 'Oprávnění', 'permissions_intro' => 'Pokud je povoleno, tato oprávnění budou mít přednost před všemi nastavenými oprávněními role.', 'permissions_enable' => 'Povolit vlastní oprávnění', 'permissions_save' => 'Uložit oprávnění', - 'permissions_owner' => 'Owner', + 'permissions_owner' => 'Vlastník', // Search 'search_results' => 'Výsledky hledání', - 'search_total_results_found' => 'Nalezen :count výsledek|Nalezeny :count výsledky|Nalezeny :count výsledky|Nalezeny :count výsledky|Nalezeno :count výsledků', + 'search_total_results_found' => '{1}Nalezen :count výsledek|[2,4]Nalezeny :count výsledky|[5,*]Nalezeno :count výsledků', 'search_clear' => 'Vymazat hledání', 'search_no_pages' => 'Tomuto hledání neodpovídají žádné stránky', 'search_for_term' => 'Hledat :term', @@ -62,7 +63,7 @@ return [ 'search_permissions_set' => 'Sada oprávnění', 'search_created_by_me' => 'Vytvořeno mnou', 'search_updated_by_me' => 'Aktualizováno mnou', - 'search_owned_by_me' => 'Owned by me', + 'search_owned_by_me' => 'Patřící mně', 'search_date_options' => 'Možnosti data', 'search_updated_before' => 'Aktualizováno před', 'search_updated_after' => 'Aktualizováno po', @@ -82,25 +83,26 @@ return [ 'shelves_new' => 'Nové knihovny', 'shelves_new_action' => 'Nová Knihovna', 'shelves_popular_empty' => 'Nejpopulárnější knihovny se objeví zde.', - 'shelves_new_empty' => 'Zde se objeví nejnověji vytvořené knihovny.', + 'shelves_new_empty' => 'Zde se zobrazí nejnověji vytvořené knihovny.', 'shelves_save' => 'Uložit knihovnu', 'shelves_books' => 'Knihy v této knihovně', 'shelves_add_books' => 'Přidat knihy do knihovny', - 'shelves_drag_books' => 'Knihu přidáte jejím přetažením sem.', + 'shelves_drag_books' => 'Knihu přidáte jejím přetažením sem', 'shelves_empty_contents' => 'Tato knihovna neobsahuje žádné knihy', - 'shelves_edit_and_assign' => 'Pro přidáni knih do knihovny stiskněte úprvy.', + 'shelves_edit_and_assign' => 'Upravit knihovnu a přiřadit knihy', 'shelves_edit_named' => 'Upravit knihovnu :name', 'shelves_edit' => 'Upravit knihovnu', 'shelves_delete' => 'Odstranit knihovnu', 'shelves_delete_named' => 'Odstranit knihovnu :name', - 'shelves_delete_explain' => "Toto odstraní knihovnu s názvem ‚:name‘. Obsažené knihy nebudou odstraněny.", + 'shelves_delete_explain' => "Toto odstraní knihovnu ‚:name‘. Vložené knihy nebudou odstraněny.", 'shelves_delete_confirmation' => 'Opravdu chcete odstranit tuto knihovnu?', 'shelves_permissions' => 'Oprávnění knihovny', 'shelves_permissions_updated' => 'Oprávnění knihovny byla aktualizována', - 'shelves_permissions_active' => 'Oprávnění knihovny jsou aktivní', + 'shelves_permissions_active' => 'Oprávnění knihovny byla aktivována', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy', 'shelves_copy_permissions' => 'Kopírovat oprávnění', - 'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění této knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.', + 'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.', 'shelves_copy_permission_success' => 'Oprávnění knihovny byla zkopírována na :count knih', // Books @@ -112,24 +114,24 @@ return [ 'books_recent' => 'Nedávné knihy', 'books_new' => 'Nové knihy', 'books_new_action' => 'Nová kniha', - 'books_popular_empty' => 'Zde se objeví nejoblíbenější knihy.', - 'books_new_empty' => 'Zde se objeví nejnověji vytvořené knihy.', + 'books_popular_empty' => 'Zde se zobrazí nejoblíbenější knihy.', + 'books_new_empty' => 'Zde se zobrazí nejnověji vytvořené knihy.', 'books_create' => 'Vytvořit novou knihu', 'books_delete' => 'Odstranit knihu', 'books_delete_named' => 'Odstranit knihu :bookName', - 'books_delete_explain' => 'Toto odstraní knihu s názvem ‚:bookName‘. Všechny stránky a kapitoly budou odebrány.', + 'books_delete_explain' => 'Toto odstraní knihu ‚:bookName‘. Všechny stránky a kapitoly v této knize budou také odstraněny.', 'books_delete_confirmation' => 'Opravdu chcete odstranit tuto knihu?', 'books_edit' => 'Upravit knihu', 'books_edit_named' => 'Upravit knihu :bookName', 'books_form_book_name' => 'Název knihy', 'books_save' => 'Uložit knihu', 'books_permissions' => 'Oprávnění knihy', - 'books_permissions_updated' => 'Oprávnění knihy aktualizována', - 'books_empty_contents' => 'Pro tuto knihu nebyly vytvořeny žádné stránky nebo kapitoly.', + 'books_permissions_updated' => 'Oprávnění knihy byla aktualizována', + 'books_empty_contents' => 'Pro tuto knihu nebyly vytvořeny žádné stránky ani kapitoly.', 'books_empty_create_page' => 'Vytvořit novou stránku', 'books_empty_sort_current_book' => 'Seřadit aktuální knihu', 'books_empty_add_chapter' => 'Přidat kapitolu', - 'books_permissions_active' => 'Oprávnění knihy jsou aktivní', + 'books_permissions_active' => 'Oprávnění knihy byla aktivována', 'books_search_this' => 'Prohledat tuto knihu', 'books_navigation' => 'Navigace knihy', 'books_sort' => 'Seřadit obsah knihy', @@ -137,8 +139,8 @@ return [ 'books_sort_name' => 'Seřadit podle názvu', 'books_sort_created' => 'Seřadit podle data vytvoření', 'books_sort_updated' => 'Seřadit podle data aktualizace', - 'books_sort_chapters_first' => 'Kapitoly první', - 'books_sort_chapters_last' => 'Kapitoly poslední', + 'books_sort_chapters_first' => 'Kapitoly jako první', + 'books_sort_chapters_last' => 'Kapitoly jako poslední', 'books_sort_show_other' => 'Zobrazit ostatní knihy', 'books_sort_save' => 'Uložit nové pořadí', @@ -149,20 +151,20 @@ return [ 'chapters_popular' => 'Populární kapitoly', 'chapters_new' => 'Nová kapitola', 'chapters_create' => 'Vytvořit novou kapitolu', - 'chapters_delete' => 'Smazat kapitolu', - 'chapters_delete_named' => 'Smazat kapitolu :chapterName', - 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.', - 'chapters_delete_confirm' => 'Opravdu chcete tuto kapitolu smazat?', + 'chapters_delete' => 'Odstranit kapitolu', + 'chapters_delete_named' => 'Odstranit kapitolu :chapterName', + 'chapters_delete_explain' => 'Toto odstraní kapitolu ‚:chapterName‘. Všechny stránky v této kapitole budou také odstraněny.', + 'chapters_delete_confirm' => 'Opravdu chcete odstranit tuto kapitolu?', 'chapters_edit' => 'Upravit kapitolu', 'chapters_edit_named' => 'Upravit kapitolu :chapterName', 'chapters_save' => 'Uložit kapitolu', 'chapters_move' => 'Přesunout kapitolu', 'chapters_move_named' => 'Přesunout kapitolu :chapterName', 'chapter_move_success' => 'Kapitola přesunuta do knihy :bookName', - 'chapters_permissions' => 'Práva kapitoly', + 'chapters_permissions' => 'Oprávnění kapitoly', 'chapters_empty' => 'Tato kapitola neobsahuje žádné stránky', - 'chapters_permissions_active' => 'Účinná práva kapitoly', - 'chapters_permissions_success' => 'Práva kapitoly aktualizována', + 'chapters_permissions_active' => 'Oprávnění kapitoly byla aktivována', + 'chapters_permissions_success' => 'Oprávnění kapitoly byla aktualizována', 'chapters_search_this' => 'Prohledat tuto kapitolu', // Pages @@ -173,9 +175,9 @@ return [ 'pages_new' => 'Nová stránka', 'pages_attachments' => 'Přílohy', 'pages_navigation' => 'Obsah stránky', - 'pages_delete' => 'Smazat stránku', - 'pages_delete_named' => 'Smazat stránku :pageName', - 'pages_delete_draft_named' => 'Smazat koncept stránky :pageName', + 'pages_delete' => 'Odstranit stránku', + 'pages_delete_named' => 'Odstranit stránku :pageName', + 'pages_delete_draft_named' => 'Odstranit koncept stránky :pageName', 'pages_delete_draft' => 'Odstranit koncept stránky', 'pages_delete_success' => 'Stránka odstraněna', 'pages_delete_draft_success' => 'Koncept stránky odstraněn', @@ -206,17 +208,17 @@ return [ 'pages_move_success' => 'Stránka přesunuta do ":parentName"', 'pages_copy' => 'Kopírovat stránku', 'pages_copy_desination' => 'Cíl kopírování', - 'pages_copy_success' => 'Stránka byla úspěšně zkopírována', + 'pages_copy_success' => 'Stránka byla zkopírována', 'pages_permissions' => 'Oprávnění stránky', - 'pages_permissions_success' => 'Oprávnění stránky aktualizována', + 'pages_permissions_success' => 'Oprávnění stránky byla aktualizována', 'pages_revision' => 'Revize', 'pages_revisions' => 'Revize stránky', 'pages_revisions_named' => 'Revize stránky pro :pageName', 'pages_revision_named' => 'Revize stránky pro :pageName', - 'pages_revision_restored_from' => 'Restored from #:id; :summary', + 'pages_revision_restored_from' => 'Obnoveno z #:id; :summary', 'pages_revisions_created_by' => 'Vytvořeno uživatelem', 'pages_revisions_date' => 'Datum revize', - 'pages_revisions_number' => 'Č.', + 'pages_revisions_number' => 'Č. ', 'pages_revisions_numbered' => 'Revize č. :id', 'pages_revisions_numbered_changes' => 'Změny revize č. :id', 'pages_revisions_changelog' => 'Protokol změn', @@ -227,7 +229,7 @@ return [ 'pages_revisions_none' => 'Tato stránka nemá žádné revize', 'pages_copy_link' => 'Kopírovat odkaz', 'pages_edit_content_link' => 'Upravit obsah', - 'pages_permissions_active' => 'Účinná práva stránky', + 'pages_permissions_active' => 'Oprávnění stránky byla aktivována', 'pages_initial_revision' => 'První vydání', 'pages_initial_name' => 'Nová stránka', 'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen před :timeDiff.', @@ -263,8 +265,8 @@ return [ 'attachments_link' => 'Připojit odkaz', 'attachments_set_link' => 'Nastavit odkaz', 'attachments_delete' => 'Jste si jisti, že chcete odstranit tuto přílohu?', - 'attachments_dropzone' => 'Přetáhněte sem soubory myší nebo sem kliknětě pro vybrání souboru.', - 'attachments_no_files' => 'Žádné soubory nebyli nahrány', + 'attachments_dropzone' => 'Přetáhněte sem soubory myší nebo sem klikněte pro vybrání souboru', + 'attachments_no_files' => 'Žádné soubory nebyly nahrány', 'attachments_explain_link' => 'Můžete pouze připojit odkaz pokud nechcete nahrávat soubor přímo. Může to být odkaz na jinou stránku nebo na soubor v cloudu.', 'attachments_link_name' => 'Název odkazu', 'attachment_link' => 'Odkaz na přílohu', @@ -274,13 +276,13 @@ return [ 'attachments_insert_link' => 'Přidat odkaz na přílohu do stránky', 'attachments_edit_file' => 'Upravit soubor', 'attachments_edit_file_name' => 'Název souboru', - 'attachments_edit_drop_upload' => 'Přetáhněte sem soubor myší nebo klikněte pro nahrání nového a následné přepsání starého.', + 'attachments_edit_drop_upload' => 'Přetáhněte sem soubor myší nebo klikněte pro nahrání nového souboru a následné přepsání starého', 'attachments_order_updated' => 'Pořadí příloh aktualizováno', 'attachments_updated_success' => 'Podrobnosti příloh aktualizovány', - 'attachments_deleted' => 'Příloha byla smazána', - 'attachments_file_uploaded' => 'Soubor byl úspěšně nahrán', - 'attachments_file_updated' => 'Soubor byl úspěšně aktualizován', - 'attachments_link_attached' => 'Odkaz úspěšně přiložen ke stránce', + 'attachments_deleted' => 'Příloha byla odstraněna', + 'attachments_file_uploaded' => 'Soubor byl nahrán', + 'attachments_file_updated' => 'Soubor byl aktualizován', + 'attachments_link_attached' => 'Odkaz byl přiložen ke stránce', 'templates' => 'Šablony', 'templates_set_as_template' => 'Tato stránka je šablona', 'templates_explain_set_as_template' => 'Tuto stránku můžete nastavit jako šablonu, aby byl její obsah využit při vytváření dalších stránek. Ostatní uživatelé budou moci použít tuto šablonu, pokud mají oprávnění k zobrazení této stránky.', @@ -300,7 +302,7 @@ return [ 'comment' => 'Komentář', 'comments' => 'Komentáře', 'comment_add' => 'Přidat komentář', - 'comment_placeholder' => 'Zanechat komentář zde', + 'comment_placeholder' => 'Zde zadejte komentář', 'comment_count' => '{0} Bez komentářů|{1} 1 komentář|[2,4] :count komentáře|[5,*] :count komentářů', 'comment_save' => 'Uložit komentář', 'comment_saving' => 'Ukládání komentáře...', @@ -308,15 +310,15 @@ return [ 'comment_new' => 'Nový komentář', 'comment_created' => 'komentováno :createDiff', 'comment_updated' => 'Aktualizováno :updateDiff uživatelem :username', - 'comment_deleted_success' => 'Komentář smazán', + 'comment_deleted_success' => 'Komentář odstraněn', 'comment_created_success' => 'Komentář přidán', 'comment_updated_success' => 'Komentář aktualizován', - 'comment_delete_confirm' => 'Opravdu chcete smazat tento komentář?', + 'comment_delete_confirm' => 'Opravdu chcete odstranit tento komentář?', 'comment_in_reply_to' => 'Odpověď na :commentId', // Revision - 'revision_delete_confirm' => 'Opravdu chcete smazat tuto revizi?', + 'revision_delete_confirm' => 'Opravdu chcete odstranit tuto revizi?', 'revision_restore_confirm' => 'Jste si jisti, že chcete obnovit tuto revizi? Aktuální obsah stránky bude nahrazen.', - 'revision_delete_success' => 'Revize smazána', - 'revision_cannot_delete_latest' => 'Nelze smazat poslední revizi.' + 'revision_delete_success' => 'Revize odstraněna', + 'revision_cannot_delete_latest' => 'Nelze odstranit poslední revizi.' ]; diff --git a/resources/lang/cs/errors.php b/resources/lang/cs/errors.php index 102a1f88b..c948cafd1 100644 --- a/resources/lang/cs/errors.php +++ b/resources/lang/cs/errors.php @@ -5,14 +5,14 @@ return [ // Permissions - 'permission' => 'Nemáte povolení přistupovat na dotazovanou stránku.', + 'permission' => 'Nemáte povolení přistupovat na požadovanou stránku.', 'permissionJson' => 'Nemáte povolení k provedení požadované akce.', // Auth 'error_user_exists_different_creds' => 'Uživatel s emailem :email již existuje ale s jinými přihlašovacími údaji.', 'email_already_confirmed' => 'Emailová adresa již byla potvrzena. Zkuste se přihlásit.', 'email_confirmation_invalid' => 'Tento potvrzovací odkaz již neplatí nebo už byl použit. Zkuste prosím registraci znovu.', - 'email_confirmation_expired' => 'Potvrzovací odkaz už neplatí, email s novým odkazem už byl poslán.', + 'email_confirmation_expired' => 'Tento potvrzovací odkaz již neplatí, byl Vám odeslán nový potvrzovací e-mail.', 'email_confirmation_awaiting' => 'E-mailová adresa pro používaný účet musí být potvrzena', 'ldap_fail_anonymous' => 'Přístup k adresáři LDAP jako anonymní uživatel (anonymous bind) selhal', 'ldap_fail_authed' => 'Přístup k adresáři LDAP pomocí zadaného jména (dn) a hesla selhal', @@ -50,7 +50,7 @@ return [ // Pages 'page_draft_autosave_fail' => 'Nepovedlo se uložit koncept. Než stránku uložíte, ujistěte se, že jste připojeni k internetu.', - 'page_custom_home_deletion' => 'Nelze smazat tuto stránku, protože je nastavena jako uvítací stránka.', + 'page_custom_home_deletion' => 'Nelze odstranit tuto stránku, protože je nastavena jako uvítací stránka', // Entities 'entity_not_found' => 'Prvek nenalezen', @@ -60,39 +60,39 @@ return [ 'chapter_not_found' => 'Kapitola nenalezena', 'selected_book_not_found' => 'Vybraná kniha nebyla nalezena', 'selected_book_chapter_not_found' => 'Zvolená kniha nebo kapitola nebyla nalezena', - 'guests_cannot_save_drafts' => 'Návštěvníci z řad veřejnosti nemohou ukládat koncepty.', + 'guests_cannot_save_drafts' => 'Nepřihlášení návštěvníci nemohou ukládat koncepty', // Users - 'users_cannot_delete_only_admin' => 'Nemůžete smazat posledního administrátora', - 'users_cannot_delete_guest' => 'Uživatele host není možno smazat', + 'users_cannot_delete_only_admin' => 'Nemůžete odstranit posledního administrátora', + 'users_cannot_delete_guest' => 'Uživatele Host není možno odstranit', // Roles 'role_cannot_be_edited' => 'Tuto roli nelze editovat', - 'role_system_cannot_be_deleted' => 'Toto je systémová role a nelze jí smazat.', - 'role_registration_default_cannot_delete' => 'Tuto roli nelze smazat dokud je nastavená jako výchozí role pro registraci nových uživatelů.', + 'role_system_cannot_be_deleted' => 'Toto je systémová role a nelze jí odstranit', + 'role_registration_default_cannot_delete' => 'Tuto roli nelze odstranit dokud je nastavená jako výchozí role pro registraci nových uživatelů', 'role_cannot_remove_only_admin' => 'Tento uživatel má roli administrátora. Přiřaďte roli administrátora někomu jinému než jí odeberete zde.', // Comments - 'comment_list' => 'Při dotahování komentářů nastala chyba.', + 'comment_list' => 'Při načítání komentářů nastala chyba.', 'cannot_add_comment_to_draft' => 'Nemůžete přidávat komentáře ke konceptu.', 'comment_add' => 'Při přidávání / aktualizaci komentáře nastala chyba.', - 'comment_delete' => 'Při mazání komentáře nastala chyba.', + 'comment_delete' => 'Při odstraňování komentáře nastala chyba.', 'empty_comment' => 'Nemůžete přidat prázdný komentář.', // Error pages '404_page_not_found' => 'Stránka nenalezena', - 'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte nebyla nalezena.', + 'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte, nebyla nalezena.', 'sorry_page_not_found_permission_warning' => 'Pokud očekáváte, že by stránka měla existovat, možná jen nemáte oprávnění pro její zobrazení.', - 'image_not_found' => 'Image Not Found', - 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', - 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', + 'image_not_found' => 'Obrázek nenalezen', + 'image_not_found_subtitle' => 'Omlouváme se, ale obrázek, který hledáte, nebyl nalezen.', + 'image_not_found_details' => 'Pokud očekáváte, že by obrázel měl existovat, tak byl zřejmě již odstraněn.', 'return_home' => 'Návrat domů', 'error_occurred' => 'Nastala chyba', 'app_down' => ':appName je momentálně vypnutá', - 'back_soon' => 'Brzy naběhne.', + 'back_soon' => 'Brzy bude opět v provozu.', // API errors - 'api_no_authorization_found' => 'V požadavku nebyla nalezen žádný autorizační token', + 'api_no_authorization_found' => 'V požadavku nebyl nalezen žádný autorizační token', 'api_bad_authorization_format' => 'V požadavku byl nalezen autorizační token, ale jeho formát se zdá být chybný', 'api_user_token_not_found' => 'Pro zadaný autorizační token nebyl nalezen žádný odpovídající API token', 'api_incorrect_token_secret' => 'Poskytnutý Token Secret neodpovídá použitému API tokenu', diff --git a/resources/lang/cs/passwords.php b/resources/lang/cs/passwords.php index cc5669d48..40b12b607 100644 --- a/resources/lang/cs/passwords.php +++ b/resources/lang/cs/passwords.php @@ -6,10 +6,10 @@ */ return [ - 'password' => 'Heslo musí mít alespoň osm znaků a musí odpovídat potvrzení.', + 'password' => 'Heslo musí mít alespoň osm znaků a musí odpovídat potvrzení hesla.', 'user' => "Nemůžeme nalézt uživatele s touto e-mailovou adresou.", - 'token' => 'Token pro obnovení hesla je neplatný pro tuto e-mailovou adresu.', - 'sent' => 'Poslali jsme vám e-mail s odkazem pro obnovení hesla!', + 'token' => 'Token pro obnovení hesla není platný pro tuto e-mailovou adresu.', + 'sent' => 'Poslali jsme Vám e-mail s odkazem pro obnovení hesla!', 'reset' => 'Vaše heslo bylo obnoveno!', ]; diff --git a/resources/lang/cs/settings.php b/resources/lang/cs/settings.php index 86b94956f..e7711bfd1 100644 --- a/resources/lang/cs/settings.php +++ b/resources/lang/cs/settings.php @@ -35,13 +35,13 @@ return [ 'app_primary_color' => 'Hlavní barva aplikace', 'app_primary_color_desc' => 'Nastaví hlavní barvu aplikace včetně panelů, tlačítek a odkazů.', 'app_homepage' => 'Úvodní stránka aplikace', - 'app_homepage_desc' => 'Vyberte si zobrazení, které se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.', + 'app_homepage_desc' => 'Zvolte si zobrazení, které se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.', 'app_homepage_select' => 'Zvolte stránku', - 'app_footer_links' => 'Footer Links', - 'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".', - 'app_footer_links_label' => 'Link Label', - 'app_footer_links_url' => 'Link URL', - 'app_footer_links_add' => 'Add Footer Link', + 'app_footer_links' => 'Odkazy v zápatí', + 'app_footer_links_desc' => 'Přidejte odkazy, které se zobrazí v zápatí webu. Ty se zobrazí ve spodní části většiny stránek, včetně těch, které nevyžadují přihlášení. K použití překladů definovaných systémem můžete použít štítek „trans::“. Například: Použití „trans::common.privacy_policy“ přeloží text na „Zásady ochrany osobních údajů“ a „trans::common.terms_of_service“ poskytne přeložený text „Podmínky služby“.', + 'app_footer_links_label' => 'Text odkazu', + 'app_footer_links_url' => 'URL odkazu', + 'app_footer_links_add' => 'Přidat odkaz do zápatí', 'app_disable_comments' => 'Vypnutí komentářů', 'app_disable_comments_toggle' => 'Vypnout komentáře', 'app_disable_comments_desc' => 'Vypne komentáře napříč všemi stránkami.
      Existující komentáře se přestanou zobrazovat.', @@ -73,10 +73,10 @@ return [ 'maint' => 'Údržba', 'maint_image_cleanup' => 'Pročistění obrázků', 'maint_image_cleanup_desc' => "Prohledá stránky a jejich revize, aby zjistil, které obrázky a kresby jsou momentálně používány a které jsou zbytečné. Zajistěte plnou zálohu databáze a obrázků než se do toho pustíte.", - 'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions', + 'maint_delete_images_only_in_revisions' => 'Odstranit i obrázky, které se vyskytují pouze ve starých revizích stránky', 'maint_image_cleanup_run' => 'Spustit pročištění', - 'maint_image_cleanup_warning' => 'Nalezeno :count potenciálně nepoužitých obrázků. Jste si jistí, že je chcete smazat?', - 'maint_image_cleanup_success' => 'Potenciálně nepoužité obrázky byly smazány. Celkem :count.', + 'maint_image_cleanup_warning' => 'Nalezeno :count potenciálně nepoužitých obrázků. Jste si jisti, že je chcete odstranit?', + 'maint_image_cleanup_success' => 'Nalezeno :count potenciálně nepoužitých obrázků a všechny byly odstraněny!', 'maint_image_cleanup_nothing_found' => 'Žádné potenciálně nepoužité obrázky nebyly nalezeny. Nic nebylo smazáno.', 'maint_send_test_email' => 'Odeslat zkušební e-mail', 'maint_send_test_email_desc' => 'Toto pošle zkušební e-mail na vaši e-mailovou adresu uvedenou ve vašem profilu.', @@ -85,27 +85,29 @@ return [ 'maint_send_test_email_mail_subject' => 'Testovací e-mail', 'maint_send_test_email_mail_greeting' => 'Zdá se, že posílání e-mailů funguje!', 'maint_send_test_email_mail_text' => 'Gratulujeme! Protože jste dostali tento e-mail, zdá se, že nastavení e-mailů je v pořádku.', - 'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.', - 'maint_recycle_bin_open' => 'Open Recycle Bin', + 'maint_recycle_bin_desc' => 'Odstraněné knihovny, knihy, kapitoly a stránky se přesouvají do Koše, aby je bylo možné obnovit nebo trvale smazat. Starší položky v koši mohou být po čase automaticky odstraněny v závislosti na konfiguraci systému.', + 'maint_recycle_bin_open' => 'Otevřít Koš', // Recycle Bin - 'recycle_bin' => 'Recycle Bin', - 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'recycle_bin_deleted_item' => 'Deleted Item', - 'recycle_bin_deleted_by' => 'Deleted By', - 'recycle_bin_deleted_at' => 'Deletion Time', - 'recycle_bin_permanently_delete' => 'Permanently Delete', - 'recycle_bin_restore' => 'Restore', - 'recycle_bin_contents_empty' => 'The recycle bin is currently empty', - 'recycle_bin_empty' => 'Empty Recycle Bin', - 'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?', - 'recycle_bin_destroy_list' => 'Items to be Destroyed', - 'recycle_bin_restore_list' => 'Items to be Restored', - 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', - 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', - 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', - 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', + 'recycle_bin' => 'Koš', + 'recycle_bin_desc' => 'Zde můžete obnovit položky, které byly odstraněny, nebo zvolit jejich trvalé odstranění ze systému. Tento seznam je nefiltrovaný, na rozdíl od podobných seznamů aktivit v systému, kde jsou použity filtry podle oprávnění.', + 'recycle_bin_deleted_item' => 'Odstraněná položka', + 'recycle_bin_deleted_parent' => 'Nadřazená položka', + 'recycle_bin_deleted_by' => 'Odstranil/a', + 'recycle_bin_deleted_at' => 'Čas odstranění', + 'recycle_bin_permanently_delete' => 'Trvale odstranit', + 'recycle_bin_restore' => 'Obnovit', + 'recycle_bin_contents_empty' => 'Koš je nyní prázdný', + 'recycle_bin_empty' => 'Vysypat Koš', + 'recycle_bin_empty_confirm' => 'Toto trvale odstraní všechny položky v Koši včetně obsahu vloženého v každé položce. Opravdu chcete vysypat Koš?', + 'recycle_bin_destroy_confirm' => 'Tato akce trvale odstraní ze systému tuto položku spolu s veškerým vloženým obsahem a tento obsah nebudete moci obnovit. Opravdu chcete tuto položku trvale odstranit?', + 'recycle_bin_destroy_list' => 'Položky k trvalému odstranění', + 'recycle_bin_restore_list' => 'Položky k obnovení', + 'recycle_bin_restore_confirm' => 'Tato akce obnoví odstraněnou položku včetně veškerého vloženého obsahu do původního umístění. Pokud bylo původní umístění od té doby odstraněno a nyní je v Koši, bude také nutné obnovit nadřazenou položku.', + 'recycle_bin_restore_deleted_parent' => 'Nadřazená položka této položky byla také odstraněna. Ty zůstanou odstraněny, dokud nebude obnoven i nadřazený objekt.', + 'recycle_bin_restore_parent' => 'Obnovit nadřazenu položku', + 'recycle_bin_destroy_notification' => 'Celkem odstraněno :count položek z Koše.', + 'recycle_bin_restore_notification' => 'Celkem obnoveno :count položek z Koše.', // Audit Log 'audit' => 'Protokol auditu', @@ -116,7 +118,7 @@ return [ 'audit_deleted_item_name' => 'Jméno: :name', 'audit_table_user' => 'Uživatel', 'audit_table_event' => 'Událost', - 'audit_table_related' => 'Related Item or Detail', + 'audit_table_related' => 'Související položka nebo detail', 'audit_table_date' => 'Datum aktivity', 'audit_date_from' => 'Časový rozsah od', 'audit_date_to' => 'Časový rozsah do', @@ -125,17 +127,18 @@ return [ 'roles' => 'Role', 'role_user_roles' => 'Uživatelské role', 'role_create' => 'Vytvořit novou roli', - 'role_create_success' => 'Role byla úspěšně vytvořena', - 'role_delete' => 'Smazat roli', - 'role_delete_confirm' => 'Role \':roleName\' bude smazána.', + 'role_create_success' => 'Role byla vytvořena', + 'role_delete' => 'Odstranit roli', + 'role_delete_confirm' => 'Role \':roleName\' bude odstraněna.', 'role_delete_users_assigned' => 'Role je přiřazena :userCount uživatelům. Pokud jim chcete náhradou přidělit jinou roli, zvolte jednu z následujících.', 'role_delete_no_migration' => "Nepřiřazovat uživatelům náhradní roli", - 'role_delete_sure' => 'Opravdu chcete tuto roli smazat?', - 'role_delete_success' => 'Role byla úspěšně smazána', + 'role_delete_sure' => 'Opravdu chcete tuto roli odstranit?', + 'role_delete_success' => 'Role byla odstraněna', 'role_edit' => 'Upravit roli', 'role_details' => 'Detaily role', 'role_name' => 'Název role', 'role_desc' => 'Stručný popis role', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Přihlašovací identifikátory třetích stran', 'role_system' => 'Systémová oprávnění', 'role_manage_users' => 'Správa uživatelů', @@ -145,15 +148,16 @@ return [ 'role_manage_page_templates' => 'Správa šablon stránek', 'role_access_api' => 'Přístup k systémovému API', 'role_manage_settings' => 'Správa nastavení aplikace', + 'role_export_content' => 'Export content', 'role_asset' => 'Obsahová oprávnění', 'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.', - 'role_asset_desc' => 'Tato práva řídí přístup k obsahu napříč systémem. Specifická práva na knihách, kapitolách a stránkách převáží tato nastavení.', + 'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.', 'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.', 'role_all' => 'Vše', 'role_own' => 'Vlastní', 'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány', 'role_save' => 'Uložit roli', - 'role_update_success' => 'Role úspěšně aktualizována', + 'role_update_success' => 'Role byla aktualizována', 'role_users' => 'Uživatelé mající tuto roli', 'role_users_none' => 'Žádný uživatel nemá tuto roli', @@ -162,7 +166,7 @@ return [ 'user_profile' => 'Profil uživatele', 'users_add_new' => 'Přidat nového uživatele', 'users_search' => 'Vyhledávání uživatelů', - 'users_latest_activity' => 'Latest Activity', + 'users_latest_activity' => 'Nedávná aktivita', 'users_details' => 'Údaje o uživateli', 'users_details_desc' => 'Nastavte zobrazované jméno a e-mailovou adresu pro tohoto uživatele. E-mailová adresa bude použita pro přihlášení do aplikace.', 'users_details_desc_no_email' => 'Nastavte zobrazované jméno pro tohoto uživatele, aby jej ostatní uživatele poznali.', @@ -178,51 +182,55 @@ return [ 'users_system_public' => 'Symbolizuje každého nepřihlášeného návštěvníka, který navštívil aplikaci. Nelze ho použít k přihlášení ale je přiřazen automaticky nepřihlášeným.', 'users_delete' => 'Smazat uživatele', 'users_delete_named' => 'Odstranit uživatele :userName', - 'users_delete_warning' => 'Uživatel \':userName\' bude zcela smazán ze systému.', + 'users_delete_warning' => 'Uživatel \':userName\' bude zcela odstraněn ze systému.', 'users_delete_confirm' => 'Opravdu chcete tohoto uživatele smazat?', - 'users_migrate_ownership' => 'Migrate Ownership', - 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.', - 'users_none_selected' => 'No user selected', - 'users_delete_success' => 'User successfully removed', + 'users_migrate_ownership' => 'Převést vlastnictví', + 'users_migrate_ownership_desc' => 'Zde zvolte jiného uživatele, pokud chcete, aby se stal vlastníkem všech položek aktuálně vlastněných tímto uživatelem.', + 'users_none_selected' => 'Nebyl zvolen žádný uživatel', + 'users_delete_success' => 'Uživatel byl odstraněn', 'users_edit' => 'Upravit uživatele', 'users_edit_profile' => 'Upravit profil', 'users_edit_success' => 'Uživatel byl úspěšně aktualizován', 'users_avatar' => 'Obrázek uživatele', - 'users_avatar_desc' => 'Vyberte obrázek, který bude reprezentovat tohoto uživatele. Měl by být přibližně 256px velký ve tvaru čtverce.', + 'users_avatar_desc' => 'Zvolte obrázek, který bude reprezentovat tohoto uživatele. Měl by být přibližně 256px velký ve tvaru čtverce.', 'users_preferred_language' => 'Preferovaný jazyk', 'users_preferred_language_desc' => 'Tato volba ovlivní pouze jazyk používaný v uživatelském rozhraní aplikace. Volba nemá vliv na žádný uživateli vytvářený obsah.', 'users_social_accounts' => 'Sociální účty', 'users_social_accounts_info' => 'Zde můžete přidat vaše účty ze sociálních sítí pro pohodlnější přihlašování. Odpojení účtů neznamená, že tato aplikace ztratí práva číst detaily z vašeho účtu. Zakázat této aplikaci přístup k detailům vašeho účtu musíte přímo ve svém profilu na dané sociální síti.', 'users_social_connect' => 'Připojit účet', 'users_social_disconnect' => 'Odpojit účet', - 'users_social_connected' => 'Účet :socialAccount byl úspěšně připojen k vašemu profilu.', - 'users_social_disconnected' => 'Účet :socialAccount byl úspěšně odpojen od vašeho profilu.', + 'users_social_connected' => 'Účet :socialAccount byl připojen k vašemu profilu.', + 'users_social_disconnected' => 'Účet :socialAccount byl odpojen od vašeho profilu.', 'users_api_tokens' => 'API Tokeny', 'users_api_tokens_none' => 'Tento uživatel nemá vytvořené žádné API Tokeny', 'users_api_tokens_create' => 'Vytvořit Token', 'users_api_tokens_expires' => 'Vyprší', 'users_api_tokens_docs' => 'API Dokumentace', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens - 'user_api_token_create' => 'Vytvořit API Klíč', + 'user_api_token_create' => 'Vytvořit API Token', 'user_api_token_name' => 'Název', 'user_api_token_name_desc' => 'Zadejte srozumitelný název tokenu, který vám později může pomoci připomenout účel, za jakým jste token vytvářeli.', 'user_api_token_expiry' => 'Platný do', 'user_api_token_expiry_desc' => 'Zadejte datum, kdy platnost tokenu vyprší. Po tomto datu nebudou požadavky, které používají tento token, fungovat. Pokud ponecháte pole prázdné, bude tokenu nastavena platnost na dalších 100 let.', 'user_api_token_create_secret_message' => 'Ihned po vytvoření tokenu Vám bude vygenerován a zobrazen "Token ID" a "Token Secret". Upozorňujeme, že "Token Secret" bude možné zobrazit pouze jednou, ujistěte se, že si jej poznamenáte a uložíte na bezpečné místo před tím, než budete pokračovat dále.', - 'user_api_token_create_success' => 'API klíč úspěšně vytvořen', - 'user_api_token_update_success' => 'API klíč úspěšně aktualizován', - 'user_api_token' => 'API Klíč', + 'user_api_token_create_success' => 'API Token byl vytvořen', + 'user_api_token_update_success' => 'API Token byl aktualizován', + 'user_api_token' => 'API Token', 'user_api_token_id' => 'Token ID', - 'user_api_token_id_desc' => 'Toto je neupravitelný systémový identifikátor generovaný pro tento klíč, který musí být uveden v API requestu.', + 'user_api_token_id_desc' => 'Toto je neupravitelný systémový identifikátor generovaný pro tento Token, který musí být uveden v API requestu.', 'user_api_token_secret' => 'Token Secret', - 'user_api_token_secret_desc' => 'Toto je systémem generovaný "secret" pro tento klíč, který musí být v API requestech. Toto bude zobrazeno pouze jednou, takže si uložte tuto hodnotu na bezpečné místo.', + 'user_api_token_secret_desc' => 'Toto je systémem generovaný "Secret" pro tento Token, který musí být v API requestech. Toto bude zobrazeno pouze jednou, takže si uložte tuto hodnotu na bezpečné místo.', 'user_api_token_created' => 'Token vytvořen :timeAgo', 'user_api_token_updated' => 'Token aktualizován :timeAgo', 'user_api_token_delete' => 'Odstranit Token', - 'user_api_token_delete_warning' => 'Tímto plně smažete tento API klíč s názvem \':tokenName\' ze systému.', - 'user_api_token_delete_confirm' => 'Opravdu chcete odstranit tento API klíč?', - 'user_api_token_delete_success' => 'API Klíč úspěšně odstraněn', + 'user_api_token_delete_warning' => 'Tímto plně odstraníte tento API Token s názvem \':tokenName\' ze systému.', + 'user_api_token_delete_confirm' => 'Opravdu chcete odstranit tento API Token?', + 'user_api_token_delete_success' => 'API Token byl odstraněn', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/cs/validation.php b/resources/lang/cs/validation.php index 1a94b9e24..ea7eebdf9 100644 --- a/resources/lang/cs/validation.php +++ b/resources/lang/cs/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.', 'alpha_num' => ':attribute může obsahovat pouze písmena a číslice.', 'array' => ':attribute musí být pole.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute musí být datum před :date.', 'between' => [ 'numeric' => ':attribute musí být hodnota mezi :min a :max.', @@ -60,7 +61,7 @@ return [ 'array' => ':attribute by měl obsahovat méně než :value položek.', ], 'lte' => [ - 'numeric' => ':attribute musí být menší nebo rovno než :value.', + 'numeric' => ':attribute musí být menší nebo rovno :value.', 'file' => 'Velikost souboru :attribute musí být menší než :value kB.', 'string' => ':attribute nesmí být delší než :value znaků.', 'array' => ':attribute by měl obsahovat maximálně :value položek.', @@ -89,7 +90,7 @@ return [ 'required_without' => ':attribute musí být vyplněno pokud :values není vyplněno.', 'required_without_all' => ':attribute musí být vyplněno pokud není žádné z :values zvoleno.', 'same' => ':attribute a :other se musí shodovat.', - 'safe_url' => 'The provided link may not be safe.', + 'safe_url' => 'Zadaný odkaz může být nebezpečný.', 'size' => [ 'numeric' => ':attribute musí být přesně :size.', 'file' => ':attribute musí mít přesně :size Kilobytů.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute musí být řetězec znaků.', 'timezone' => ':attribute musí být platná časová zóna.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute musí být unikátní.', 'url' => 'Formát :attribute je neplatný.', 'uploaded' => 'Nahrávání :attribute se nezdařilo.', diff --git a/resources/lang/da/activities.php b/resources/lang/da/activities.php index 614d1a8ac..a81dde3ec 100644 --- a/resources/lang/da/activities.php +++ b/resources/lang/da/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'kommenterede til', 'permissions_update' => 'Tilladelser opdateret', diff --git a/resources/lang/da/auth.php b/resources/lang/da/auth.php index 8ea585174..f16bbf47f 100644 --- a/resources/lang/da/auth.php +++ b/resources/lang/da/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Velkommen til :appName!', 'user_invite_page_text' => 'For at færdiggøre din konto og få adgang skal du indstille en adgangskode, der bruges til at logge ind på :appName ved fremtidige besøg.', 'user_invite_page_confirm_button' => 'Bekræft adgangskode', - 'user_invite_success' => 'Adgangskode indstillet, du har nu adgang til :appName!' + 'user_invite_success' => 'Adgangskode indstillet, du har nu adgang til :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/da/common.php b/resources/lang/da/common.php index 0504f5a10..829ab9382 100644 --- a/resources/lang/da/common.php +++ b/resources/lang/da/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Nulstil', 'remove' => 'Fjern', 'add' => 'Tilføj', + 'configure' => 'Configure', 'fullscreen' => 'Fuld skærm', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Ingen aktivitet at vise', 'no_items' => 'Intet indhold tilgængeligt', 'back_to_top' => 'Tilbage til toppen', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Vis/skjul detaljer', 'toggle_thumbnails' => 'Vis/skjul miniaturer', 'details' => 'Detaljer', diff --git a/resources/lang/da/entities.php b/resources/lang/da/entities.php index 36e290d86..a61f80906 100644 --- a/resources/lang/da/entities.php +++ b/resources/lang/da/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Indeholdt webfil', 'export_pdf' => 'PDF-fil', 'export_text' => 'Almindelig tekstfil', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Rettigheder', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Reoltilladelser', 'shelves_permissions_updated' => 'Reoltilladelser opdateret', 'shelves_permissions_active' => 'Aktive reoltilladelser', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopier tilladelser til bøger', 'shelves_copy_permissions' => 'Kopier tilladelser', 'shelves_copy_permissions_explain' => 'Dette vil anvende de aktuelle tilladelsesindstillinger på denne boghylde på alle bøger indeholdt i. Før aktivering skal du sikre dig, at ændringer i tilladelserne til denne boghylde er blevet gemt.', diff --git a/resources/lang/da/settings.php b/resources/lang/da/settings.php index 7bbc31edb..212a727a6 100644 --- a/resources/lang/da/settings.php +++ b/resources/lang/da/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Papirkurv', 'recycle_bin_desc' => 'Her kan du gendanne elementer, der er blevet slettet eller vælge at permanent fjerne dem fra systemet. Denne liste er ufiltreret, i modsætning til lignende aktivitetslister i systemet, hvor tilladelsesfiltre anvendes.', 'recycle_bin_deleted_item' => 'Slettet element', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Slettet af', 'recycle_bin_deleted_at' => 'Sletningstidspunkt', 'recycle_bin_permanently_delete' => 'Slet permanent', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Elementer der skal gendannes', 'recycle_bin_restore_confirm' => 'Denne handling vil gendanne det slettede element, herunder alle underelementer, til deres oprindelige placering. Hvis den oprindelige placering siden er blevet slettet, og nu er i papirkurven, vil det overordnede element også skulle gendannes.', 'recycle_bin_restore_deleted_parent' => 'Det overordnede element til dette element er også blevet slettet. Disse vil forblive slettet indtil det overordnede også er gendannet.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Slettede :count elementer fra papirkurven.', 'recycle_bin_restore_notification' => 'Gendannede :count elementer fra papirkurven.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Rolledetaljer', 'role_name' => 'Rollenavn', 'role_desc' => 'Kort beskrivelse af rolle', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Eksterne godkendelses-IDer', 'role_system' => 'Systemtilladelser', 'role_manage_users' => 'Administrere brugere', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Administrer side-skabeloner', 'role_access_api' => 'Tilgå system-API', 'role_manage_settings' => 'Administrer app-indstillinger', + 'role_export_content' => 'Export content', 'role_asset' => 'Tilladelser for medier og "assets"', 'roles_system_warning' => 'Vær opmærksom på, at adgang til alle af de ovennævnte tre tilladelser, kan give en bruger mulighed for at ændre deres egne brugerrettigheder eller brugerrettigheder for andre i systemet. Tildel kun roller med disse tilladelser til betroede brugere.', 'role_asset_desc' => 'Disse tilladelser kontrollerer standardadgang til medier og "assets" i systemet. Tilladelser til bøger, kapitler og sider tilsidesætter disse tilladelser.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Opret Token', 'users_api_tokens_expires' => 'Udløber', 'users_api_tokens_docs' => 'API-dokumentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Opret API-token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/da/validation.php b/resources/lang/da/validation.php index 6c11f2e0f..0635f14a7 100644 --- a/resources/lang/da/validation.php +++ b/resources/lang/da/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute må kun bestå af bogstaver, tal, binde- og under-streger.', 'alpha_num' => ':attribute må kun indeholde bogstaver og tal.', 'array' => ':attribute skal være et array.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute skal være en dato før :date.', 'between' => [ 'numeric' => ':attribute skal være mellem :min og :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute skal være tekst.', 'timezone' => ':attribute skal være en gyldig zone.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute er allerede i brug.', 'url' => ':attribute-formatet er ugyldigt.', 'uploaded' => 'Filen kunne ikke oploades. Serveren accepterer muligvis ikke filer af denne størrelse.', diff --git a/resources/lang/de/activities.php b/resources/lang/de/activities.php index 52c99168f..87dd3ee8b 100644 --- a/resources/lang/de/activities.php +++ b/resources/lang/de/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt', 'favourite_remove_notification' => '":name" wurde aus Ihren Favoriten entfernt', + // MFA + 'mfa_setup_method_notification' => 'Multi-Faktor-Methode erfolgreich konfiguriert', + 'mfa_remove_method_notification' => 'Multi-Faktor-Methode erfolgreich entfernt', + // Other 'commented_on' => 'hat einen Kommentar hinzugefügt', 'permissions_update' => 'hat die Berechtigungen aktualisiert', diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php index 1f5a49cbd..efe82680d 100644 --- a/resources/lang/de/auth.php +++ b/resources/lang/de/auth.php @@ -6,7 +6,7 @@ */ return [ - 'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.', + 'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', // Login & Register @@ -20,7 +20,7 @@ return [ 'username' => 'Benutzername', 'email' => 'E-Mail', 'password' => 'Passwort', - 'password_confirm' => 'Passwort bestätigen', + 'password_confirm' => 'Passwort bestätigen', 'password_hint' => 'Mindestlänge: 7 Zeichen', 'forgot_password' => 'Passwort vergessen?', 'remember_me' => 'Angemeldet bleiben', @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Willkommen bei :appName!', 'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft zum Einloggen benötigt.', 'user_invite_page_confirm_button' => 'Passwort wiederholen', - 'user_invite_success' => 'Passwort gesetzt, Sie haben nun Zugriff auf :appName!' + 'user_invite_success' => 'Passwort gesetzt, Sie haben nun Zugriff auf :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Multi-Faktor-Authentifizierung einrichten', + 'mfa_setup_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.', + 'mfa_setup_configured' => 'Bereits konfiguriert', + 'mfa_setup_reconfigure' => 'Umkonfigurieren', + 'mfa_setup_remove_confirmation' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode entfernen möchten?', + 'mfa_setup_action' => 'Einrichtung', + 'mfa_backup_codes_usage_limit_warning' => 'Sie haben weniger als 5 Backup-Codes übrig, Bitte erstellen und speichern Sie ein neues Set bevor Sie keine Codes mehr haben, um zu verhindern, dass Sie von Ihrem Konto gesperrt werden.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Code', + 'mfa_option_backup_codes_desc' => 'Speichern Sie sicher eine Reihe von einmaligen Backup-Codes, die Sie eingeben können, um Ihre Identität zu überprüfen.', + 'mfa_gen_confirm_and_enable' => 'Bestätigen und aktivieren', + 'mfa_gen_backup_codes_title' => 'Backup-Codes einrichten', + 'mfa_gen_backup_codes_desc' => 'Speichern Sie die folgende Liste der Codes an einem sicheren Ort. Wenn Sie auf das System zugreifen, können Sie einen der Codes als zweiten Authentifizierungsmechanismus verwenden.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Jeder Code kann nur einmal verwendet werden', + 'mfa_gen_totp_title' => 'Mobile App einrichten', + 'mfa_gen_totp_desc' => 'Um Mehrfach-Faktor-Authentifizierung nutzen zu können, benötigen Sie eine mobile Anwendung, die TOTP unterstützt, wie Google Authenticator, Authy oder Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scannen Sie den QR-Code unten mit Ihrer bevorzugten Authentifizierungs-App, um loszulegen.', + 'mfa_gen_totp_verify_setup' => 'Setup überprüfen', + 'mfa_gen_totp_verify_setup_desc' => 'Überprüfen Sie, dass alles funktioniert, indem Sie einen Code in Ihrer Authentifizierungs-App in das Eingabefeld unten eingeben:', + 'mfa_gen_totp_provide_code_here' => 'Geben Sie hier Ihre App generierten Code ein', + 'mfa_verify_access' => 'Zugriff überprüfen', + 'mfa_verify_access_desc' => 'Ihr Benutzerkonto erfordert, dass Sie Ihre Identität über eine zusätzliche Verifikationsebene bestätigen, bevor Sie den Zugriff gewähren. Überprüfen Sie mit einer Ihrer konfigurierten Methoden, um fortzufahren.', + 'mfa_verify_no_methods' => 'Keine Methoden konfiguriert', + 'mfa_verify_no_methods_desc' => 'Es konnten keine Mehrfach-Faktor-Authentifizierungsmethoden für Ihr Konto gefunden werden. Sie müssen mindestens eine Methode einrichten, bevor Sie Zugriff erhalten.', + 'mfa_verify_use_totp' => 'Mit einer mobilen App verifizieren', + 'mfa_verify_use_backup_codes' => 'Mit einem Backup-Code überprüfen', + 'mfa_verify_backup_code' => 'Backup-Code', + 'mfa_verify_backup_code_desc' => 'Geben Sie einen Ihrer verbleibenden Backup-Codes unten ein:', + 'mfa_verify_backup_code_enter_here' => 'Backup-Code hier eingeben', + 'mfa_verify_totp_desc' => 'Geben Sie den Code ein, der mit Ihrer mobilen App generiert wurde:', + 'mfa_setup_login_notification' => 'Multi-Faktor-Methode konfiguriert. Bitte melden Sie sich jetzt erneut mit der konfigurierten Methode an.', ]; \ No newline at end of file diff --git a/resources/lang/de/common.php b/resources/lang/de/common.php index f46537ced..85808b67d 100644 --- a/resources/lang/de/common.php +++ b/resources/lang/de/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Zurücksetzen', 'remove' => 'Entfernen', 'add' => 'Hinzufügen', + 'configure' => 'Konfigurieren', 'fullscreen' => 'Vollbild', 'favourite' => 'Favorit', 'unfavourite' => 'Kein Favorit', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Keine Aktivitäten zum Anzeigen', 'no_items' => 'Keine Einträge gefunden.', 'back_to_top' => 'nach oben', + 'skip_to_main_content' => 'Direkt zum Hauptinhalt', 'toggle_details' => 'Details zeigen/verstecken', 'toggle_thumbnails' => 'Thumbnails zeigen/verstecken', 'details' => 'Details', diff --git a/resources/lang/de/entities.php b/resources/lang/de/entities.php index d11c8dbaf..30efc7984 100644 --- a/resources/lang/de/entities.php +++ b/resources/lang/de/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'HTML-Datei', 'export_pdf' => 'PDF-Datei', 'export_text' => 'Textdatei', + 'export_md' => 'Markdown-Datei', // Permissions and restrictions 'permissions' => 'Berechtigungen', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Regal-Berechtigungen', 'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert', 'shelves_permissions_active' => 'Regal-Berechtigungen aktiv', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch', 'shelves_copy_permissions' => 'Berechtigungen kopieren', 'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.', diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php index a853a0b62..c9e589347 100644 --- a/resources/lang/de/settings.php +++ b/resources/lang/de/settings.php @@ -95,6 +95,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin' => 'Papierkorb', 'recycle_bin_desc' => 'Hier können Sie gelöschte Elemente wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.', 'recycle_bin_deleted_item' => 'Gelöschtes Element', + 'recycle_bin_deleted_parent' => 'Übergeordnet', 'recycle_bin_deleted_by' => 'Gelöscht von', 'recycle_bin_deleted_at' => 'Löschzeitpunkt', 'recycle_bin_permanently_delete' => 'Dauerhaft löschen', @@ -107,6 +108,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin_restore_list' => 'Zu wiederherzustellende Elemente', 'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird das gelöschte Element einschließlich aller untergeordneten Elemente an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch das übergeordnete Element wiederhergestellt werden.', 'recycle_bin_restore_deleted_parent' => 'Das übergeordnete Elements wurde ebenfalls gelöscht. Dieses Element wird weiterhin als gelöscht zählen, bis auch das übergeordnete Element wiederhergestellt wurde.', + 'recycle_bin_restore_parent' => 'Elternteil wiederherstellen', 'recycle_bin_destroy_notification' => ':count Elemente wurden aus dem Papierkorb gelöscht.', 'recycle_bin_restore_notification' => ':count Elemente wurden aus dem Papierkorb wiederhergestellt.', @@ -139,6 +141,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_details' => 'Rollendetails', 'role_name' => 'Rollenname', 'role_desc' => 'Kurzbeschreibung der Rolle', + 'role_mfa_enforced' => 'Benötigt Mehrfach-Faktor-Authentifizierung', 'role_external_auth_id' => 'Externe Authentifizierungs-IDs', 'role_system' => 'System-Berechtigungen', 'role_manage_users' => 'Benutzer verwalten', @@ -148,6 +151,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_manage_page_templates' => 'Seitenvorlagen verwalten', 'role_access_api' => 'Systemzugriffs-API', 'role_manage_settings' => 'Globaleinstellungen verwalten', + 'role_export_content' => 'Export content', 'role_asset' => 'Berechtigungen', 'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.', 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', @@ -205,6 +209,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_api_tokens_create' => 'Token erstellen', 'users_api_tokens_expires' => 'Endet', 'users_api_tokens_docs' => 'API Dokumentation', + 'users_mfa' => 'Multi-Faktor-Authentifizierung', + 'users_mfa_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.', + 'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert', + 'users_mfa_configure' => 'Methoden konfigurieren', // API Tokens 'user_api_token_create' => 'Neuen API-Token erstellen', @@ -250,6 +258,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php index 50f8a76a3..5d08c241a 100644 --- a/resources/lang/de/validation.php +++ b/resources/lang/de/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.', 'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.', 'array' => ':attribute muss ein Array sein.', + 'backup_codes' => 'Der angegebene Code ist ungültig oder wurde bereits verwendet.', 'before' => ':attribute muss ein Datum vor :date sein.', 'between' => [ 'numeric' => ':attribute muss zwischen :min und :max liegen.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute muss eine Zeichenkette sein.', 'timezone' => ':attribute muss eine valide zeitzone sein.', + 'totp' => 'Der angegebene Code ist ungültig oder abgelaufen.', 'unique' => ':attribute wird bereits verwendet.', 'url' => ':attribute ist kein valides Format.', 'uploaded' => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.', diff --git a/resources/lang/de_informal/activities.php b/resources/lang/de_informal/activities.php index fbaa9a211..fec33bec2 100644 --- a/resources/lang/de_informal/activities.php +++ b/resources/lang/de_informal/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt', 'favourite_remove_notification' => '":name" wurde aus Ihren Favoriten entfernt', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'kommentiert', 'permissions_update' => 'aktualisierte Berechtigungen', diff --git a/resources/lang/de_informal/auth.php b/resources/lang/de_informal/auth.php index 918598533..d09008e5a 100644 --- a/resources/lang/de_informal/auth.php +++ b/resources/lang/de_informal/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Willkommen bei :appName!', 'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft zum Einloggen benötigt.', 'user_invite_page_confirm_button' => 'Passwort bestätigen', - 'user_invite_success' => 'Das Passwort wurde gesetzt, du hast nun Zugriff auf :appName!' + 'user_invite_success' => 'Das Passwort wurde gesetzt, du hast nun Zugriff auf :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/de_informal/common.php b/resources/lang/de_informal/common.php index 993d3e70c..594ee519d 100644 --- a/resources/lang/de_informal/common.php +++ b/resources/lang/de_informal/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Zurücksetzen', 'remove' => 'Entfernen', 'add' => 'Hinzufügen', + 'configure' => 'Configure', 'fullscreen' => 'Vollbild', 'favourite' => 'Favorit', 'unfavourite' => 'Kein Favorit', - 'next' => 'Next', - 'previous' => 'Previous', + 'next' => 'Nächste', + 'previous' => 'Vorheriges', // Sort Options 'sort_options' => 'Sortieroptionen', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Keine Aktivitäten zum Anzeigen', 'no_items' => 'Keine Einträge gefunden.', 'back_to_top' => 'nach oben', + 'skip_to_main_content' => 'Direkt zum Hauptinhalt', 'toggle_details' => 'Details zeigen/verstecken', 'toggle_thumbnails' => 'Thumbnails zeigen/verstecken', 'details' => 'Details', diff --git a/resources/lang/de_informal/entities.php b/resources/lang/de_informal/entities.php index 5377b1017..4bc530a52 100644 --- a/resources/lang/de_informal/entities.php +++ b/resources/lang/de_informal/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'HTML-Datei', 'export_pdf' => 'PDF-Datei', 'export_text' => 'Textdatei', + 'export_md' => 'Markdown-Datei', // Permissions and restrictions 'permissions' => 'Berechtigungen', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Regal-Berechtigungen', 'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert', 'shelves_permissions_active' => 'Regal-Berechtigungen aktiv', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch', 'shelves_copy_permissions' => 'Berechtigungen kopieren', 'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfe vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.', diff --git a/resources/lang/de_informal/settings.php b/resources/lang/de_informal/settings.php index 5c915c537..3dbc320f9 100644 --- a/resources/lang/de_informal/settings.php +++ b/resources/lang/de_informal/settings.php @@ -95,6 +95,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin' => 'Papierkorb', 'recycle_bin_desc' => 'Hier können Sie gelöschte Einträge wiederherstellen oder sie dauerhaft aus dem System entfernen. Diese Liste ist nicht gefiltert, im Gegensatz zu ähnlichen Aktivitätslisten im System, wo Berechtigungsfilter angewendet werden.', 'recycle_bin_deleted_item' => 'Gelöschter Eintrag', + 'recycle_bin_deleted_parent' => 'Übergeordnet', 'recycle_bin_deleted_by' => 'Gelöscht von', 'recycle_bin_deleted_at' => 'Löschzeitpunkt', 'recycle_bin_permanently_delete' => 'Dauerhaft löschen', @@ -107,6 +108,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'recycle_bin_restore_list' => 'Wiederherzustellende Einträge', 'recycle_bin_restore_confirm' => 'Mit dieser Aktion wird der gelöschte Eintrag einschließlich aller untergeordneten Einträge an seinen ursprünglichen Ort wiederherstellen. Wenn der ursprüngliche Ort gelöscht wurde und sich nun im Papierkorb befindet, muss auch der übergeordnete Eintrag wiederhergestellt werden.', 'recycle_bin_restore_deleted_parent' => 'Der übergeordnete Eintrag wurde ebenfalls gelöscht. Dieser Eintrag wird weiterhin als gelöscht zählen, bis auch der übergeordnete Eintrag wiederhergestellt wurde.', + 'recycle_bin_restore_parent' => 'Elternteil wiederherstellen', 'recycle_bin_destroy_notification' => ':count Einträge wurden aus dem Papierkorb gelöscht.', 'recycle_bin_restore_notification' => ':count Einträge wurden aus dem Papierkorb wiederhergestellt.', @@ -139,6 +141,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_details' => 'Rollendetails', 'role_name' => 'Rollenname', 'role_desc' => 'Kurzbeschreibung der Rolle', + 'role_mfa_enforced' => 'Benötigt Mehrfach-Faktor-Authentifizierung', 'role_external_auth_id' => 'Externe Authentifizierungs-IDs', 'role_system' => 'System-Berechtigungen', 'role_manage_users' => 'Benutzer verwalten', @@ -148,6 +151,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_manage_page_templates' => 'Seitenvorlagen verwalten', 'role_access_api' => 'Systemzugriffs-API', 'role_manage_settings' => 'Globaleinstellungen verwalten', + 'role_export_content' => 'Export content', 'role_asset' => 'Berechtigungen', 'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.', 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.', @@ -205,6 +209,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_api_tokens_create' => 'Token erstellen', 'users_api_tokens_expires' => 'Endet', 'users_api_tokens_docs' => 'API Dokumentation', + 'users_mfa' => 'Multi-Faktor-Authentifizierung', + 'users_mfa_desc' => 'Richten Sie Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.', + 'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert', + 'users_mfa_configure' => 'Methoden konfigurieren', // API Tokens 'user_api_token_create' => 'Neuen API-Token erstellen', @@ -250,6 +258,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/de_informal/validation.php b/resources/lang/de_informal/validation.php index 42456da6e..7eb385ac9 100644 --- a/resources/lang/de_informal/validation.php +++ b/resources/lang/de_informal/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.', 'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.', 'array' => ':attribute muss ein Array sein.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute muss ein Datum vor :date sein.', 'between' => [ 'numeric' => ':attribute muss zwischen :min und :max liegen.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute muss eine Zeichenkette sein.', 'timezone' => ':attribute muss eine valide zeitzone sein.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute wird bereits verwendet.', 'url' => ':attribute ist kein valides Format.', 'uploaded' => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.', diff --git a/resources/lang/en/activities.php b/resources/lang/en/activities.php index 5917de2cf..50bda60bd 100644 --- a/resources/lang/en/activities.php +++ b/resources/lang/en/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'commented on', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php index d64fce93a..e4d4c425b 100644 --- a/resources/lang/en/auth.php +++ b/resources/lang/en/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Welcome to :appName!', 'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.', 'user_invite_page_confirm_button' => 'Confirm Password', - 'user_invite_success' => 'Password set, you now have access to :appName!' + 'user_invite_success' => 'Password set, you now have access to :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/en/common.php b/resources/lang/en/common.php index 1861869e3..f93fb034b 100644 --- a/resources/lang/en/common.php +++ b/resources/lang/en/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Reset', 'remove' => 'Remove', 'add' => 'Add', + 'configure' => 'Configure', 'fullscreen' => 'Fullscreen', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 462402f33..1be9c18e0 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Contained Web File', 'export_pdf' => 'PDF File', 'export_text' => 'Plain Text File', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Permissions', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Bookshelf Permissions', 'shelves_permissions_updated' => 'Bookshelf Permissions Updated', 'shelves_permissions_active' => 'Bookshelf Permissions Active', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books', 'shelves_copy_permissions' => 'Copy Permissions', 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.', diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 8a7946b12..4c1ae1345 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Role Details', 'role_name' => 'Role Name', 'role_desc' => 'Short Description of Role', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'System Permissions', 'role_manage_users' => 'Manage users', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Manage page templates', 'role_access_api' => 'Access system API', 'role_manage_settings' => 'Manage app settings', + 'role_export_content' => 'Export content', 'role_asset' => 'Asset Permissions', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Create Token', 'users_api_tokens_expires' => 'Expires', 'users_api_tokens_docs' => 'API Documentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 4031de2ae..1963b0df2 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', 'alpha_num' => 'The :attribute may only contain letters and numbers.', 'array' => 'The :attribute must be an array.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'The :attribute must be a date before :date.', 'between' => [ 'numeric' => 'The :attribute must be between :min and :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'The :attribute must be a string.', 'timezone' => 'The :attribute must be a valid zone.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'The :attribute has already been taken.', 'url' => 'The :attribute format is invalid.', 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', diff --git a/resources/lang/es/activities.php b/resources/lang/es/activities.php index b64ada3a4..a3449269d 100644 --- a/resources/lang/es/activities.php +++ b/resources/lang/es/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '".name" ha sido añadido a sus favoritos', 'favourite_remove_notification' => '".name" ha sido eliminado de sus favoritos', + // MFA + 'mfa_setup_method_notification' => 'Método de Autenticación en Dos Pasos configurado correctamente', + 'mfa_remove_method_notification' => 'Método de Autenticación en Dos Pasos eliminado correctamente', + // Other 'commented_on' => 'comentada el', 'permissions_update' => 'permisos actualizados', diff --git a/resources/lang/es/auth.php b/resources/lang/es/auth.php index 25fc5b650..96f0b384e 100644 --- a/resources/lang/es/auth.php +++ b/resources/lang/es/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => '¡Bienvenido a :appName!', 'user_invite_page_text' => 'Para completar la cuenta y tener acceso es necesario que configure una contraseña que se utilizará para entrar en :appName en futuros accesos.', 'user_invite_page_confirm_button' => 'Confirmar Contraseña', - 'user_invite_success' => '¡Contraseña guardada, ya tiene acceso a :appName!' + 'user_invite_success' => '¡Contraseña guardada, ya tiene acceso a :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Configurar Autenticación en Dos Pasos', + 'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.', + 'mfa_setup_configured' => 'Ya está configurado', + 'mfa_setup_reconfigure' => 'Reconfigurar', + 'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación de dos pasos?', + 'mfa_setup_action' => 'Configuración', + 'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.', + 'mfa_option_totp_title' => 'Aplicación para móviles', + 'mfa_option_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Códigos de Respaldo', + 'mfa_option_backup_codes_desc' => 'Almacena de forma segura un conjunto de códigos de respaldo de un solo uso que puedes introducir para verificar tu identidad.', + 'mfa_gen_confirm_and_enable' => 'Confirmar y Activar', + 'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo', + 'mfa_gen_backup_codes_desc' => 'Guarda la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrás usar uno de los códigos como un segundo mecanismo de autenticación.', + 'mfa_gen_backup_codes_download' => 'Descargar Códigos', + 'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez', + 'mfa_gen_totp_title' => 'Configuración de Aplicación móvil', + 'mfa_gen_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.', + 'mfa_gen_totp_verify_setup' => 'Verificar Configuración', + 'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:', + 'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación', + 'mfa_verify_access' => 'Verificar Acceso', + 'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.', + 'mfa_verify_no_methods' => 'No hay Métodos Configurados', + 'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.', + 'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil', + 'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo', + 'mfa_verify_backup_code' => 'Códigos de Respaldo', + 'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:', + 'mfa_verify_backup_code_enter_here' => 'Introduce el código de respaldo aquí', + 'mfa_verify_totp_desc' => 'Introduzca el código, generado con tu aplicación móvil, a continuación:', + 'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicia sesión de nuevo utilizando el método configurado.', ]; \ No newline at end of file diff --git a/resources/lang/es/common.php b/resources/lang/es/common.php index d19277402..b8514a876 100644 --- a/resources/lang/es/common.php +++ b/resources/lang/es/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Resetear', 'remove' => 'Remover', 'add' => 'Añadir', + 'configure' => 'Configurar', 'fullscreen' => 'Pantalla completa', 'favourite' => 'Añadir a favoritos', 'unfavourite' => 'Eliminar de favoritos', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Ninguna actividad para mostrar', 'no_items' => 'No hay elementos disponibles', 'back_to_top' => 'Volver arriba', + 'skip_to_main_content' => 'Ir al contenido principal', 'toggle_details' => 'Alternar detalles', 'toggle_thumbnails' => 'Alternar miniaturas', 'details' => 'Detalles', diff --git a/resources/lang/es/entities.php b/resources/lang/es/entities.php index 2478d8689..3325557b7 100644 --- a/resources/lang/es/entities.php +++ b/resources/lang/es/entities.php @@ -6,9 +6,9 @@ return [ // Shared - 'recently_created' => 'Recientemente creado', - 'recently_created_pages' => 'Páginas recientemente creadas', - 'recently_updated_pages' => 'Páginas recientemente actualizadas', + 'recently_created' => 'Creado Recientemente', + 'recently_created_pages' => 'Páginas creadas recientemente', + 'recently_updated_pages' => 'Páginas actualizadas recientemente', 'recently_created_chapters' => 'Capítulos recientemente creados', 'recently_created_books' => 'Libros recientemente creados', 'recently_created_shelves' => 'Estantes recientemente creados', @@ -36,6 +36,7 @@ return [ 'export_html' => 'Archivo web', 'export_pdf' => 'Archivo PDF', 'export_text' => 'Archivo de texto', + 'export_md' => 'Archivo Markdown', // Permissions and restrictions 'permissions' => 'Permisos', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permisos del estante', 'shelves_permissions_updated' => 'Permisos del estante actualizados', 'shelves_permissions_active' => 'Permisos del estante activos', + 'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros contenidos. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción a continuación.', 'shelves_copy_permissions_to_books' => 'Copiar permisos a los libros', 'shelves_copy_permissions' => 'Copiar permisos', 'shelves_copy_permissions_explain' => 'Esto aplicará los ajustes de permisos de este estante para todos sus libros. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante han sido guardados.', diff --git a/resources/lang/es/settings.php b/resources/lang/es/settings.php index 0010e7cae..1ffaf187c 100644 --- a/resources/lang/es/settings.php +++ b/resources/lang/es/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Papelera de Reciclaje', 'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.', 'recycle_bin_deleted_item' => 'Elemento Eliminado', + 'recycle_bin_deleted_parent' => 'Superior', 'recycle_bin_deleted_by' => 'Eliminado por', 'recycle_bin_deleted_at' => 'Fecha de eliminación', 'recycle_bin_permanently_delete' => 'Eliminar permanentemente', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Elementos a restaurar', 'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.', 'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.', + 'recycle_bin_restore_parent' => 'Restaurar Superior', 'recycle_bin_destroy_notification' => 'Eliminados :count artículos de la papelera de reciclaje.', 'recycle_bin_restore_notification' => 'Restaurados :count artículos desde la papelera de reciclaje.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detalles de rol', 'role_name' => 'Nombre de rol', 'role_desc' => 'Descripción corta de rol', + 'role_mfa_enforced' => 'Requiere Autenticación en Dos Pasos', 'role_external_auth_id' => 'ID externo de autenticación', 'role_system' => 'Permisos de sistema', 'role_manage_users' => 'Gestionar usuarios', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Administrar plantillas', 'role_access_api' => 'API de sistema de acceso', 'role_manage_settings' => 'Gestionar ajustes de la aplicación', + 'role_export_content' => 'Exportar contenido', 'role_asset' => 'Permisos de contenido', 'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario alterar sus propios privilegios o los privilegios de otros en el sistema. Sólo asignar roles con estos permisos a usuarios de confianza.', 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los contenidos del sistema. Los permisos de Libros, Capítulos y Páginas sobreescribiran estos permisos.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Crear token', 'users_api_tokens_expires' => 'Expira', 'users_api_tokens_docs' => 'Documentación API', + 'users_mfa' => 'Autenticación en Dos Pasos', + 'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.', + 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', + 'users_mfa_configure' => 'Configurar Métodos', // API Tokens 'user_api_token_create' => 'Crear token API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/es/validation.php b/resources/lang/es/validation.php index 450e92375..177eb812c 100644 --- a/resources/lang/es/validation.php +++ b/resources/lang/es/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.', 'alpha_num' => 'El :attribute solo puede contener letras y números.', 'array' => 'El :attribute debe de ser un array.', + 'backup_codes' => 'El código suministrado no es válido o ya ha sido utilizado.', 'before' => 'El :attribute debe ser una fecha anterior a :date.', 'between' => [ 'numeric' => 'El :attribute debe estar entre :min y :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'El atributo :attribute debe ser una cadena de texto.', 'timezone' => 'El atributo :attribute debe ser una zona válida.', + 'totp' => 'El código suministrado no es válido o ya ha expirado.', 'unique' => 'El atributo :attribute ya ha sido tomado.', 'url' => 'El atributo :attribute tiene un formato inválido.', 'uploaded' => 'El archivo no ha podido subirse. Es posible que el servidor no acepte archivos de este tamaño.', diff --git a/resources/lang/es_AR/activities.php b/resources/lang/es_AR/activities.php index 2ee087b71..861115fc5 100644 --- a/resources/lang/es_AR/activities.php +++ b/resources/lang/es_AR/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '".name" se añadió a sus favoritos', 'favourite_remove_notification' => '".name" se eliminó de sus favoritos', + // MFA + 'mfa_setup_method_notification' => 'Método de Autenticación en Dos Pasos configurado correctamente', + 'mfa_remove_method_notification' => 'Método de Autenticación en Dos Pasos eliminado correctamente', + // Other 'commented_on' => 'comentado', 'permissions_update' => 'permisos actualizados', diff --git a/resources/lang/es_AR/auth.php b/resources/lang/es_AR/auth.php index 2f957f46d..c57b26746 100644 --- a/resources/lang/es_AR/auth.php +++ b/resources/lang/es_AR/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Bienvenido a :appName!', 'user_invite_page_text' => 'Para finalizar la cuenta y tener acceso debe establcer una contraseña que utilizará para ingresar a :appName en visitas futuras.', 'user_invite_page_confirm_button' => 'Confirmar Contraseña', - 'user_invite_success' => 'Contraseña establecida, ahora tiene acceso a :appName!' + 'user_invite_success' => 'Contraseña establecida, ahora tiene acceso a :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Configurar Autenticación en Dos Pasos', + 'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.', + 'mfa_setup_configured' => 'Ya está configurado', + 'mfa_setup_reconfigure' => 'Reconfigurar', + 'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación de dos pasos?', + 'mfa_setup_action' => 'Configuración', + 'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.', + 'mfa_option_totp_title' => 'Aplicación para móviles', + 'mfa_option_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Códigos de Respaldo', + 'mfa_option_backup_codes_desc' => 'Almacena de forma segura un conjunto de códigos de respaldo de un solo uso que puedes introducir para verificar tu identidad.', + 'mfa_gen_confirm_and_enable' => 'Confirmar y Activar', + 'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo', + 'mfa_gen_backup_codes_desc' => 'Guarda la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrás usar uno de los códigos como un segundo mecanismo de autenticación.', + 'mfa_gen_backup_codes_download' => 'Descargar Códigos', + 'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez', + 'mfa_gen_totp_title' => 'Configuración de Aplicación móvil', + 'mfa_gen_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.', + 'mfa_gen_totp_verify_setup' => 'Verificar Configuración', + 'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:', + 'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación', + 'mfa_verify_access' => 'Verificar Acceso', + 'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.', + 'mfa_verify_no_methods' => 'No hay Métodos Configurados', + 'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.', + 'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil', + 'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo', + 'mfa_verify_backup_code' => 'Códigos de Respaldo', + 'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:', + 'mfa_verify_backup_code_enter_here' => 'Introduce el código de respaldo aquí', + 'mfa_verify_totp_desc' => 'Introduzca el código, generado con tu aplicación móvil, a continuación:', + 'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicia sesión de nuevo utilizando el método configurado.', ]; \ No newline at end of file diff --git a/resources/lang/es_AR/common.php b/resources/lang/es_AR/common.php index 190d99c1c..05c76cb11 100644 --- a/resources/lang/es_AR/common.php +++ b/resources/lang/es_AR/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Restablecer', 'remove' => 'Remover', 'add' => 'Agregar', + 'configure' => 'Configurar', 'fullscreen' => 'Pantalla completa', 'favourite' => 'Favoritos', 'unfavourite' => 'Eliminar de favoritos', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Ninguna actividad para mostrar', 'no_items' => 'No hay elementos disponibles', 'back_to_top' => 'Volver arriba', + 'skip_to_main_content' => 'Ir al contenido principal', 'toggle_details' => 'Alternar detalles', 'toggle_thumbnails' => 'Alternar miniaturas', 'details' => 'Detalles', diff --git a/resources/lang/es_AR/entities.php b/resources/lang/es_AR/entities.php index 02c2dcbb9..5e71ba266 100644 --- a/resources/lang/es_AR/entities.php +++ b/resources/lang/es_AR/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Archivo web contenido', 'export_pdf' => 'Archivo PDF', 'export_text' => 'Archivo de texto plano', + 'export_md' => 'Archivo Markdown', // Permissions and restrictions 'permissions' => 'Permisos', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permisos del Estante', 'shelves_permissions_updated' => 'Permisos del Estante actualizados', 'shelves_permissions_active' => 'Permisos Activos del Estante', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copiar Permisos a los Libros', 'shelves_copy_permissions' => 'Copiar Permisos', 'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos de este estante a todos los libros contenidos en él. Antes de activarlos, asegúrese que los cambios a los permisos de este estante estén guardados.', diff --git a/resources/lang/es_AR/settings.php b/resources/lang/es_AR/settings.php index 962ef89a4..c0ea221bd 100644 --- a/resources/lang/es_AR/settings.php +++ b/resources/lang/es_AR/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Papelera de Reciclaje', 'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.', 'recycle_bin_deleted_item' => 'Elemento Eliminado', + 'recycle_bin_deleted_parent' => 'Superior', 'recycle_bin_deleted_by' => 'Eliminado por', 'recycle_bin_deleted_at' => 'Fecha de eliminación', 'recycle_bin_permanently_delete' => 'Eliminar permanentemente', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Elementos a restaurar', 'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.', 'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.', + 'recycle_bin_restore_parent' => 'Restaurar Superior', 'recycle_bin_destroy_notification' => 'Eliminados :count elementos de la papelera de reciclaje.', 'recycle_bin_restore_notification' => 'Restaurados :count elementos desde la papelera de reciclaje.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detalles de rol', 'role_name' => 'Nombre de rol', 'role_desc' => 'Descripción corta de rol', + 'role_mfa_enforced' => 'Requiere Autenticación en Dos Pasos', 'role_external_auth_id' => 'IDs de Autenticación Externa', 'role_system' => 'Permisos de sistema', 'role_manage_users' => 'Gestionar usuarios', @@ -146,6 +149,7 @@ return [ 'role_manage_page_templates' => 'Gestionar las plantillas de páginas', 'role_access_api' => 'API de sistema de acceso', 'role_manage_settings' => 'Gestionar ajustes de activos', + 'role_export_content' => 'Export content', 'role_asset' => 'Permisos de activos', 'roles_system_warning' => 'Tenga en cuenta que el acceso a cualquiera de los tres permisos anteriores puede permitir a un usuario modificar sus propios privilegios o los privilegios de otros usuarios en el sistema. Asignar roles con estos permisos sólo a usuarios de comfianza.', 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos definidos en Libros, Capítulos y Páginas ignorarán estos permisos.', @@ -203,6 +207,10 @@ return [ 'users_api_tokens_create' => 'Crear token', 'users_api_tokens_expires' => 'Expira', 'users_api_tokens_docs' => 'Documentación API', + 'users_mfa' => 'Autenticación en Dos Pasos', + 'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.', + 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', + 'users_mfa_configure' => 'Configurar Métodos', // API Tokens 'user_api_token_create' => 'Crear token API', @@ -248,6 +256,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/es_AR/validation.php b/resources/lang/es_AR/validation.php index c3f5d83dd..2cc8ed9bf 100644 --- a/resources/lang/es_AR/validation.php +++ b/resources/lang/es_AR/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.', 'alpha_num' => 'El :attribute solo puede contener letras y número.', 'array' => 'El :attribute debe de ser un array.', + 'backup_codes' => 'El código suministrado no es válido o ya ha sido utilizado.', 'before' => 'El :attribute debe ser una fecha anterior a :date.', 'between' => [ 'numeric' => 'El :attribute debe estar entre :min y :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'El atributo :attribute debe ser una cadena.', 'timezone' => 'El atributo :attribute debe ser una zona válida.', + 'totp' => 'El código suministrado no es válido o ya ha expirado.', 'unique' => 'El atributo :attribute ya ha sido tomado.', 'url' => 'El atributo :attribute tiene un formato inválido.', 'uploaded' => 'El archivo no se pudo subir. Puede ser que el servidor no acepte archivos de este tamaño.', diff --git a/resources/lang/fa/activities.php b/resources/lang/fa/activities.php index 5917de2cf..43b6b4789 100644 --- a/resources/lang/fa/activities.php +++ b/resources/lang/fa/activities.php @@ -6,48 +6,52 @@ return [ // Pages - 'page_create' => 'created page', - 'page_create_notification' => 'Page Successfully Created', - 'page_update' => 'updated page', - 'page_update_notification' => 'Page Successfully Updated', - 'page_delete' => 'deleted page', - 'page_delete_notification' => 'Page Successfully Deleted', - 'page_restore' => 'restored page', - 'page_restore_notification' => 'Page Successfully Restored', - 'page_move' => 'moved page', + 'page_create' => 'صفحه ایجاد شده', + 'page_create_notification' => 'صفحه با موفقیت ایجاد شد', + 'page_update' => 'صفحه بروز شده', + 'page_update_notification' => 'صفحه با موفقیت به روزرسانی شد', + 'page_delete' => 'حذف صفحه', + 'page_delete_notification' => 'صفحه با موفقیت حذف شد', + 'page_restore' => 'بازیابی صفحه', + 'page_restore_notification' => 'صفحه با موفقیت بازیابی شد', + 'page_move' => 'انتقال صفحه', // Chapters - 'chapter_create' => 'created chapter', - 'chapter_create_notification' => 'Chapter Successfully Created', - 'chapter_update' => 'updated chapter', - 'chapter_update_notification' => 'Chapter Successfully Updated', - 'chapter_delete' => 'deleted chapter', - 'chapter_delete_notification' => 'Chapter Successfully Deleted', - 'chapter_move' => 'moved chapter', + 'chapter_create' => 'ایجاد فصل', + 'chapter_create_notification' => 'فصل با موفقیت ایجاد شد', + 'chapter_update' => 'به روزرسانی فصل', + 'chapter_update_notification' => 'فصل با موفقیت به روزرسانی شد', + 'chapter_delete' => 'حذف فصل', + 'chapter_delete_notification' => 'فصل با موفقیت حذف شد', + 'chapter_move' => 'انتقال فصل', // Books - 'book_create' => 'created book', - 'book_create_notification' => 'Book Successfully Created', - 'book_update' => 'updated book', - 'book_update_notification' => 'Book Successfully Updated', - 'book_delete' => 'deleted book', - 'book_delete_notification' => 'Book Successfully Deleted', - 'book_sort' => 'sorted book', - 'book_sort_notification' => 'Book Successfully Re-sorted', + 'book_create' => 'ایجاد کتاب', + 'book_create_notification' => 'کتاب با موفقیت ایجاد شد', + 'book_update' => 'به روزرسانی کتاب', + 'book_update_notification' => 'کتاب با موفقیت به روزرسانی شد', + 'book_delete' => 'حذف کتاب', + 'book_delete_notification' => 'کتاب با موفقیت حذف شد', + 'book_sort' => 'مرتب سازی کتاب', + 'book_sort_notification' => 'کتاب با موفقیت مرتب سازی شد', // Bookshelves - 'bookshelf_create' => 'created Bookshelf', - 'bookshelf_create_notification' => 'Bookshelf Successfully Created', - 'bookshelf_update' => 'updated bookshelf', - 'bookshelf_update_notification' => 'Bookshelf Successfully Updated', - 'bookshelf_delete' => 'deleted bookshelf', - 'bookshelf_delete_notification' => 'Bookshelf Successfully Deleted', + 'bookshelf_create' => 'ایجاد قفسه کتاب', + 'bookshelf_create_notification' => 'قفسه کتاب با موفقیت ایجاد شد', + 'bookshelf_update' => 'به روزرسانی قفسه کتاب', + 'bookshelf_update_notification' => 'قفسه کتاب با موفقیت به روزرسانی شد', + 'bookshelf_delete' => 'حذف قفسه کتاب', + 'bookshelf_delete_notification' => 'قفسه کتاب با موفقیت حذف شد', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" به علاقه مندی های شما اضافه شد', + 'favourite_remove_notification' => '":name" از علاقه مندی های شما حذف شد', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other - 'commented_on' => 'commented on', - 'permissions_update' => 'updated permissions', + 'commented_on' => 'ثبت دیدگاه', + 'permissions_update' => 'به روزرسانی مجوزها', ]; diff --git a/resources/lang/fa/auth.php b/resources/lang/fa/auth.php index d64fce93a..4f950b77f 100644 --- a/resources/lang/fa/auth.php +++ b/resources/lang/fa/auth.php @@ -6,72 +6,107 @@ */ return [ - 'failed' => 'These credentials do not match our records.', - 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'failed' => 'مشخصات وارد شده با اطلاعات ما سازگار نیست.', + 'throttle' => 'دفعات تلاش شما برای ورود بیش از حد مجاز است. لطفا پس از :seconds ثانیه مجددا تلاش فرمایید.', // Login & Register - 'sign_up' => 'Sign up', - 'log_in' => 'Log in', - 'log_in_with' => 'Login with :socialDriver', - 'sign_up_with' => 'Sign up with :socialDriver', - 'logout' => 'Logout', + 'sign_up' => 'ثبت نام', + 'log_in' => 'ورود', + 'log_in_with' => 'ورود با :socialDriver', + 'sign_up_with' => 'ثبت نام با :socialDriver', + 'logout' => 'خروج', - 'name' => 'Name', - 'username' => 'Username', - 'email' => 'Email', - 'password' => 'Password', - 'password_confirm' => 'Confirm Password', - 'password_hint' => 'Must be over 7 characters', - 'forgot_password' => 'Forgot Password?', - 'remember_me' => 'Remember Me', - 'ldap_email_hint' => 'Please enter an email to use for this account.', - 'create_account' => 'Create Account', - 'already_have_account' => 'Already have an account?', - 'dont_have_account' => 'Don\'t have an account?', - 'social_login' => 'Social Login', - 'social_registration' => 'Social Registration', - 'social_registration_text' => 'Register and sign in using another service.', + 'name' => 'نام', + 'username' => 'نام کاربری', + 'email' => 'پست الکترونیک', + 'password' => 'کلمه عبور', + 'password_confirm' => 'تایید کلمه عبور', + 'password_hint' => 'باید بیش از 7 کاراکتر باشد', + 'forgot_password' => 'کلمه عبور خود را فراموش کرده اید؟', + 'remember_me' => 'مرا به خاطر بسپار', + 'ldap_email_hint' => 'لطفا برای استفاده از این حساب کاربری پست الکترونیک وارد نمایید.', + 'create_account' => 'ایجاد حساب کاربری', + 'already_have_account' => 'قبلا ثبت نام نموده اید؟', + 'dont_have_account' => 'حساب کاربری ندارید؟', + 'social_login' => 'ورود از طریق شبکه اجتماعی', + 'social_registration' => 'ثبت نام از طریق شبکه اجتماعی', + 'social_registration_text' => 'با استفاده از سرویس دیگری ثبت نام نموده و وارد سیستم شوید.', - 'register_thanks' => 'Thanks for registering!', - 'register_confirm' => 'Please check your email and click the confirmation button to access :appName.', - 'registrations_disabled' => 'Registrations are currently disabled', - 'registration_email_domain_invalid' => 'That email domain does not have access to this application', - 'register_success' => 'Thanks for signing up! You are now registered and signed in.', + 'register_thanks' => 'از ثبت نام شما متشکریم!', + 'register_confirm' => 'لطفا پست الکترونیک خود را بررسی نموده و برای دسترسی به:appName دکمه تایید را کلیک نمایید.', + 'registrations_disabled' => 'ثبت نام در حال حاضر غیر فعال است', + 'registration_email_domain_invalid' => 'دامنه پست الکترونیک به این برنامه دسترسی ندارد', + 'register_success' => 'از ثبت نام شما سپاسگزاریم! شما اکنون ثبت نام کرده و وارد سیستم شده اید.', // Password Reset - 'reset_password' => 'Reset Password', - 'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.', - 'reset_password_send_button' => 'Send Reset Link', - 'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.', - 'reset_password_success' => 'Your password has been successfully reset.', - 'email_reset_subject' => 'Reset your :appName password', - 'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.', - 'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.', + 'reset_password' => 'بازنشانی کلمه عبور', + 'reset_password_send_instructions' => 'پست الکترونیک خود را در کادر زیر وارد نموده تا یک پیام حاوی لینک بازنشانی کلمه عبور دریافت نمایید.', + 'reset_password_send_button' => 'ارسال لینک بازنشانی', + 'reset_password_sent' => 'در صورت موجود بودن پست الکترونیک، یک لینک بازنشانی کلمه عبور برای شما ارسال خواهد شد.', + 'reset_password_success' => 'کلمه عبور شما با موفقیت بازنشانی شد.', + 'email_reset_subject' => 'بازنشانی کلمه عبور :appName', + 'email_reset_text' => 'شما این پیام را به علت درخواست بازنشانی کلمه عبور دریافت می نمایید.', + 'email_reset_not_requested' => 'در صورتی که درخواست بازنشانی کلمه عبور از سمت شما نمی باشد، نیاز به انجام هیچ فعالیتی ندارید.', // Email Confirmation - 'email_confirm_subject' => 'Confirm your email on :appName', - 'email_confirm_greeting' => 'Thanks for joining :appName!', - 'email_confirm_text' => 'Please confirm your email address by clicking the button below:', - 'email_confirm_action' => 'Confirm Email', - 'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.', - 'email_confirm_success' => 'Your email has been confirmed!', - 'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.', + 'email_confirm_subject' => 'پست الکترونیک خود را در:appName تایید نمایید', + 'email_confirm_greeting' => 'برای پیوستن به :appName متشکریم!', + 'email_confirm_text' => 'لطفا با کلیک بر روی دکمه زیر پست الکترونیک خود را تایید نمایید:', + 'email_confirm_action' => 'تایید پست الکترونیک', + 'email_confirm_send_error' => 'تایید پست الکترونیک الزامی می باشد، اما سیستم قادر به ارسال پیام نمی باشد.', + 'email_confirm_success' => 'پست الکترونیک شما تایید گردیده است!', + 'email_confirm_resent' => 'پیام تایید پست الکترونیک مجدد ارسال گردید، لطفا صندوق ورودی خود را بررسی نمایید.', - 'email_not_confirmed' => 'Email Address Not Confirmed', - 'email_not_confirmed_text' => 'Your email address has not yet been confirmed.', - 'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.', - 'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.', - 'email_not_confirmed_resend_button' => 'Resend Confirmation Email', + 'email_not_confirmed' => 'پست الکترونیک تایید نشده است', + 'email_not_confirmed_text' => 'پست الکترونیک شما هنوز تایید نشده است.', + 'email_not_confirmed_click_link' => 'لطفا بر روی لینک موجود در پیامی که بلافاصله پس از ثبت نام ارسال شده است کلیک نمایید.', + 'email_not_confirmed_resend' => 'در صورتی که نمی توانید پیام را پیدا کنید، می توانید با ارسال فرم زیر، پیام تایید را مجدد دریافت نمایید.', + 'email_not_confirmed_resend_button' => 'ارسال مجدد تایید پست الکترونیک', // User Invite - 'user_invite_email_subject' => 'You have been invited to join :appName!', - 'user_invite_email_greeting' => 'An account has been created for you on :appName.', - 'user_invite_email_text' => 'Click the button below to set an account password and gain access:', - 'user_invite_email_action' => 'Set Account Password', - 'user_invite_page_welcome' => 'Welcome to :appName!', - 'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.', - 'user_invite_page_confirm_button' => 'Confirm Password', - 'user_invite_success' => 'Password set, you now have access to :appName!' + 'user_invite_email_subject' => 'از شما برای پیوستن به :appName دعوت شده است!', + 'user_invite_email_greeting' => 'حساب کاربری برای شما در :appName ایجاد شده است.', + 'user_invite_email_text' => 'برای تنظیم کلمه عبور و دسترسی به حساب کاربری بر روی دکمه زیر کلیک نمایید:', + 'user_invite_email_action' => 'تنظیم کلمه عبور حساب‌کاربری', + 'user_invite_page_welcome' => 'به :appName خوش آمدید!', + 'user_invite_page_text' => 'برای نهایی کردن حساب کاربری خود در :appName و دسترسی به آن، می بایست یک کلمه عبور تنظیم نمایید.', + 'user_invite_page_confirm_button' => 'تایید کلمه عبور', + 'user_invite_success' => 'کلمه عبور تنظیم شده است، شما اکنون به :appName دسترسی دارید!', + + // Multi-factor Authentication + 'mfa_setup' => 'تنظیم احراز هویت چند مرحله‌ای', + 'mfa_setup_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.', + 'mfa_setup_configured' => 'هم اکنون تنظیم شده است.', + 'mfa_setup_reconfigure' => 'تنظیم مجدد', + 'mfa_setup_remove_confirmation' => 'از حذف احراز هویت چند مرحله ای اطمینان دارید؟', + 'mfa_setup_action' => 'تنظیم', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/fa/common.php b/resources/lang/fa/common.php index d4508c3c7..6d3768de4 100644 --- a/resources/lang/fa/common.php +++ b/resources/lang/fa/common.php @@ -5,89 +5,91 @@ return [ // Buttons - 'cancel' => 'Cancel', - 'confirm' => 'Confirm', - 'back' => 'Back', - 'save' => 'Save', - 'continue' => 'Continue', - 'select' => 'Select', - 'toggle_all' => 'Toggle All', - 'more' => 'More', + 'cancel' => 'لغو', + 'confirm' => 'تایید', + 'back' => 'بازگشت', + 'save' => 'ذخیره', + 'continue' => 'ادامه', + 'select' => 'انتخاب', + 'toggle_all' => 'معکوس کردن همه', + 'more' => 'بیشتر', // Form Labels - 'name' => 'Name', - 'description' => 'Description', - 'role' => 'Role', - 'cover_image' => 'Cover image', - 'cover_image_description' => 'This image should be approx 440x250px.', + 'name' => 'نام', + 'description' => 'توضیحات', + 'role' => 'نقش', + 'cover_image' => 'تصویر روی جلد', + 'cover_image_description' => 'سایز تصویر باید 440x250 باشد.', // Actions - 'actions' => 'Actions', - 'view' => 'View', - 'view_all' => 'View All', - 'create' => 'Create', - 'update' => 'Update', - 'edit' => 'Edit', - 'sort' => 'Sort', - 'move' => 'Move', - 'copy' => 'Copy', - 'reply' => 'Reply', - 'delete' => 'Delete', - 'delete_confirm' => 'Confirm Deletion', - 'search' => 'Search', - 'search_clear' => 'Clear Search', - 'reset' => 'Reset', - 'remove' => 'Remove', - 'add' => 'Add', - 'fullscreen' => 'Fullscreen', - 'favourite' => 'Favourite', + 'actions' => 'عملیات', + 'view' => 'نمایش', + 'view_all' => 'نمایش همه', + 'create' => 'ایجاد', + 'update' => 'به‌روز رسانی', + 'edit' => 'ويرايش', + 'sort' => 'مرتب سازی', + 'move' => 'جابجایی', + 'copy' => 'کپی', + 'reply' => 'پاسخ', + 'delete' => 'حذف', + 'delete_confirm' => 'تأیید حذف', + 'search' => 'جستجو', + 'search_clear' => 'پاک کردن جستجو', + 'reset' => 'بازنشانی', + 'remove' => 'حذف', + 'add' => 'ﺍﻓﺰﻭﺩﻥ', + 'configure' => 'Configure', + 'fullscreen' => 'تمام صفحه', + 'favourite' => 'علاقه‌مندی', 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'next' => 'بعدی', + 'previous' => 'قبلى', // Sort Options - 'sort_options' => 'Sort Options', - 'sort_direction_toggle' => 'Sort Direction Toggle', - 'sort_ascending' => 'Sort Ascending', - 'sort_descending' => 'Sort Descending', - 'sort_name' => 'Name', - 'sort_default' => 'Default', - 'sort_created_at' => 'Created Date', - 'sort_updated_at' => 'Updated Date', + 'sort_options' => 'گزینه‌های مرتب سازی', + 'sort_direction_toggle' => 'معکوس کردن جهت مرتب سازی', + 'sort_ascending' => 'مرتب‌سازی صعودی', + 'sort_descending' => 'مرتب‌سازی نزولی', + 'sort_name' => 'نام', + 'sort_default' => 'پیش‎فرض', + 'sort_created_at' => 'تاریخ ایجاد', + 'sort_updated_at' => 'تاریخ بروزرسانی', // Misc - 'deleted_user' => 'Deleted User', - 'no_activity' => 'No activity to show', - 'no_items' => 'No items available', - 'back_to_top' => 'Back to top', - 'toggle_details' => 'Toggle Details', - 'toggle_thumbnails' => 'Toggle Thumbnails', - 'details' => 'Details', - 'grid_view' => 'Grid View', - 'list_view' => 'List View', - 'default' => 'Default', - 'breadcrumb' => 'Breadcrumb', + 'deleted_user' => 'کاربر حذف شده', + 'no_activity' => 'بایگانی برای نمایش وجود ندارد', + 'no_items' => 'هیچ آیتمی موجود نیست', + 'back_to_top' => 'بازگشت به بالا', + 'skip_to_main_content' => 'رفتن به محتوای اصلی', + 'toggle_details' => 'معکوس کردن اطلاعات', + 'toggle_thumbnails' => 'معکوس ریز عکس ها', + 'details' => 'جزییات', + 'grid_view' => 'نمایش شبکه‌ای', + 'list_view' => 'نمای لیست', + 'default' => 'پیش‎فرض', + 'breadcrumb' => 'مسیر جاری', // Header - 'header_menu_expand' => 'Expand Header Menu', - 'profile_menu' => 'Profile Menu', - 'view_profile' => 'View Profile', - 'edit_profile' => 'Edit Profile', - 'dark_mode' => 'Dark Mode', - 'light_mode' => 'Light Mode', + 'header_menu_expand' => 'گسترش منو', + 'profile_menu' => 'منو پروفایل', + 'view_profile' => 'مشاهده پروفایل', + 'edit_profile' => 'ویرایش پروفایل', + 'dark_mode' => 'حالت تاریک', + 'light_mode' => 'حالت روشن', // Layout tabs - 'tab_info' => 'Info', - 'tab_info_label' => 'Tab: Show Secondary Information', - 'tab_content' => 'Content', - 'tab_content_label' => 'Tab: Show Primary Content', + 'tab_info' => 'اطلاعات', + 'tab_info_label' => 'زبانه: نمایش اطلاعات ثانویه', + 'tab_content' => 'محتوا', + 'tab_content_label' => 'زبانه: نمایش محتوای اصلی', // Email Content - 'email_action_help' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:', - 'email_rights' => 'All rights reserved', + 'email_action_help' => 'اگر با دکمه بالا مشکلی دارید ، ادرس وبسایت *URLزیر را در مرورگر وب خود کپی و پیست کنید:', + 'email_rights' => 'تمام حقوق محفوظ است', // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Privacy Policy', - 'terms_of_service' => 'Terms of Service', + 'privacy_policy' => 'سیاست حفظ حریم خصوصی', + 'terms_of_service' => 'شرایط خدمات', ]; diff --git a/resources/lang/fa/components.php b/resources/lang/fa/components.php index 48a0a32fa..126bd093d 100644 --- a/resources/lang/fa/components.php +++ b/resources/lang/fa/components.php @@ -5,30 +5,30 @@ return [ // Image Manager - 'image_select' => 'Image Select', - 'image_all' => 'All', - 'image_all_title' => 'View all images', - 'image_book_title' => 'View images uploaded to this book', - 'image_page_title' => 'View images uploaded to this page', - 'image_search_hint' => 'Search by image name', - 'image_uploaded' => 'Uploaded :uploadedDate', - 'image_load_more' => 'Load More', - 'image_image_name' => 'Image Name', - 'image_delete_used' => 'This image is used in the pages below.', - 'image_delete_confirm_text' => 'Are you sure you want to delete this image?', - 'image_select_image' => 'Select Image', - 'image_dropzone' => 'Drop images or click here to upload', - 'images_deleted' => 'Images Deleted', - 'image_preview' => 'Image Preview', - 'image_upload_success' => 'Image uploaded successfully', - 'image_update_success' => 'Image details successfully updated', - 'image_delete_success' => 'Image successfully deleted', - 'image_upload_remove' => 'Remove', + 'image_select' => 'انتخاب تصویر', + '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_used' => 'این تصویر در صفحات زیر استفاده شده است.', + 'image_delete_confirm_text' => 'آیا مطمئن هستید که میخواهید این عکس را پاک کنید؟', + 'image_select_image' => 'انتخاب تصویر', + 'image_dropzone' => 'تصاویر را رها کنید یا برای بارگذاری اینجا را کلیک کنید', + 'images_deleted' => 'تصاویر حذف شده', + 'image_preview' => 'پیش نمایش تصویر', + 'image_upload_success' => 'تصویر با موفقیت بارگذاری شد', + 'image_update_success' => 'جزئیات تصویر با موفقیت به روز شد', + 'image_delete_success' => 'تصویر با موفقیت حذف شد', + 'image_upload_remove' => 'حذف', // Code Editor - 'code_editor' => 'Edit Code', - 'code_language' => 'Code Language', - 'code_content' => 'Code Content', - 'code_session_history' => 'Session History', - 'code_save' => 'Save Code', + 'code_editor' => 'ویرایش کد', + 'code_language' => 'زبان کد', + 'code_content' => 'محتوی کد', + 'code_session_history' => 'تاریخچه جلسات', + 'code_save' => 'ذخیره کد', ]; diff --git a/resources/lang/fa/entities.php b/resources/lang/fa/entities.php index 462402f33..3d45e2165 100644 --- a/resources/lang/fa/entities.php +++ b/resources/lang/fa/entities.php @@ -6,59 +6,60 @@ return [ // Shared - 'recently_created' => 'Recently Created', - 'recently_created_pages' => 'Recently Created Pages', - 'recently_updated_pages' => 'Recently Updated Pages', - 'recently_created_chapters' => 'Recently Created Chapters', - 'recently_created_books' => 'Recently Created Books', - 'recently_created_shelves' => 'Recently Created Shelves', - 'recently_update' => 'Recently Updated', - 'recently_viewed' => 'Recently Viewed', - 'recent_activity' => 'Recent Activity', - 'create_now' => 'Create one now', - 'revisions' => 'Revisions', - 'meta_revision' => 'Revision #:revisionCount', - 'meta_created' => 'Created :timeLength', - 'meta_created_name' => 'Created :timeLength by :user', - 'meta_updated' => 'Updated :timeLength', - 'meta_updated_name' => 'Updated :timeLength by :user', - 'meta_owned_name' => 'Owned by :user', - 'entity_select' => 'Entity Select', - 'images' => 'Images', - 'my_recent_drafts' => 'My Recent Drafts', - 'my_recently_viewed' => 'My Recently Viewed', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', - 'no_pages_viewed' => 'You have not viewed any pages', - 'no_pages_recently_created' => 'No pages have been recently created', - 'no_pages_recently_updated' => 'No pages have been recently updated', - 'export' => 'Export', - 'export_html' => 'Contained Web File', - 'export_pdf' => 'PDF File', - 'export_text' => 'Plain Text File', + 'recently_created' => 'اخیرا ایجاد شده', + 'recently_created_pages' => 'صفحات اخیرا ایجاد شده', + 'recently_updated_pages' => 'صفحاتی که اخیرا روزآمد شده‌اند', + 'recently_created_chapters' => 'فصل های اخیرا ایجاد شده', + 'recently_created_books' => 'کتاب های اخیرا ایجاد شده', + 'recently_created_shelves' => 'قفسه کتاب های اخیرا ایجاد شده', + 'recently_update' => 'اخیرا به روز شده', + 'recently_viewed' => 'اخیرا مشاهده شده', + 'recent_activity' => 'فعالیت های اخیر', + 'create_now' => 'اکنون یکی ایجاد کنید', + 'revisions' => 'بازبینی‌ها', + 'meta_revision' => 'بازبینی #:revisionCount', + 'meta_created' => 'ایجاد شده :timeLength', + 'meta_created_name' => 'ایجاد شده :timeLength توسط :user', + 'meta_updated' => 'به روزرسانی شده :timeLength', + 'meta_updated_name' => 'به روزرسانی شده :timeLength توسط :user', + 'meta_owned_name' => 'توسط :user ایجاد شده‌است', + 'entity_select' => 'انتخاب موجودیت', + 'images' => 'عکس ها', + 'my_recent_drafts' => 'پیش نویس های اخیر من', + 'my_recently_viewed' => 'بازدیدهای اخیر من', + 'my_most_viewed_favourites' => 'محبوب ترین موارد مورد علاقه من', + 'my_favourites' => 'مورد علاقه من', + 'no_pages_viewed' => 'شما هیچ صفحه ای را مشاهده نکرده اید', + 'no_pages_recently_created' => 'اخیرا هیچ صفحه ای ایجاد نشده است', + 'no_pages_recently_updated' => 'اخیرا هیچ صفحه ای به روزرسانی نشده است', + 'export' => 'خروجی', + 'export_html' => 'فایل وب موجود است', + 'export_pdf' => 'فایل PDF', + 'export_text' => 'پرونده متنی ساده', + 'export_md' => 'راهنما مارک‌دون', // Permissions and restrictions - 'permissions' => 'Permissions', - 'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.', - 'permissions_enable' => 'Enable Custom Permissions', - 'permissions_save' => 'Save Permissions', - 'permissions_owner' => 'Owner', + 'permissions' => 'مجوزها', + 'permissions_intro' => 'پس از فعال شدن، این مجوزها نسبت به مجوزهای تعیین شده نقش اولویت دارند.', + 'permissions_enable' => 'مجوزهای سفارشی را فعال کنید', + 'permissions_save' => 'ذخيره مجوزها', + 'permissions_owner' => 'مالک', // Search - 'search_results' => 'Search Results', + 'search_results' => 'نتایج جستجو', 'search_total_results_found' => ':count result found|:count total results found', - 'search_clear' => 'Clear Search', - 'search_no_pages' => 'No pages matched this search', - 'search_for_term' => 'Search for :term', - 'search_more' => 'More Results', - 'search_advanced' => 'Advanced Search', - 'search_terms' => 'Search Terms', - 'search_content_type' => 'Content Type', - 'search_exact_matches' => 'Exact Matches', - 'search_tags' => 'Tag Searches', - 'search_options' => 'Options', - 'search_viewed_by_me' => 'Viewed by me', - 'search_not_viewed_by_me' => 'Not viewed by me', + 'search_clear' => 'پاک کردن جستجو', + 'search_no_pages' => 'هیچ صفحه ای با این جستجو مطابقت ندارد', + 'search_for_term' => 'جستجو برای :term', + 'search_more' => 'نتایج بیشتر', + 'search_advanced' => 'جستجوی پیشرفته', + 'search_terms' => 'عبارات جستجو', + 'search_content_type' => 'نوع محتوا', + 'search_exact_matches' => 'مطابقت کامل', + 'search_tags' => 'جستجوها را برچسب بزنید', + 'search_options' => 'گزینه ها', + 'search_viewed_by_me' => 'بازدید شده به وسیله من', + 'search_not_viewed_by_me' => 'توسط من مشاهده نشده است', 'search_permissions_set' => 'Permissions set', 'search_created_by_me' => 'Created by me', 'search_updated_by_me' => 'Updated by me', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Bookshelf Permissions', 'shelves_permissions_updated' => 'Bookshelf Permissions Updated', 'shelves_permissions_active' => 'Bookshelf Permissions Active', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books', 'shelves_copy_permissions' => 'Copy Permissions', 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.', diff --git a/resources/lang/fa/errors.php b/resources/lang/fa/errors.php index eb8ba54ea..9b0281bc0 100644 --- a/resources/lang/fa/errors.php +++ b/resources/lang/fa/errors.php @@ -5,34 +5,34 @@ return [ // Permissions - 'permission' => 'You do not have permission to access the requested page.', - 'permissionJson' => 'You do not have permission to perform the requested action.', + 'permission' => 'شما مجوز مشاهده صفحه درخواست شده را ندارید.', + 'permissionJson' => 'شما مجاز به انجام این عمل نیستید.', // Auth - 'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.', - 'email_already_confirmed' => 'Email has already been confirmed, Try logging in.', - 'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.', - 'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.', - 'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed', - 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', - 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', - 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', - 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', - 'saml_already_logged_in' => 'Already logged in', - 'saml_user_not_registered' => 'The user :name is not registered and automatic registration is disabled', - 'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', - 'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.', - 'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization', - 'social_no_action_defined' => 'No action defined', - 'social_login_bad_response' => "Error received during :socialAccount login: \n:error", - 'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.', - 'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.', - 'social_account_existing' => 'This :socialAccount is already attached to your profile.', - 'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.', - 'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ', - 'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.', - 'social_driver_not_found' => 'Social driver not found', - 'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.', + 'error_user_exists_different_creds' => 'کاربری با ایمیل :email از قبل وجود دارد اما دارای اطلاعات متفاوتی می باشد.', + 'email_already_confirmed' => 'ایمیل قبلا تایید شده است، وارد سیستم شوید.', + 'email_confirmation_invalid' => 'این کلمه عبور معتبر نمی باشد و یا قبلا استفاده شده است، لطفا دوباره ثبت نام نمایید.', + 'email_confirmation_expired' => 'کلمه عبور منقضی شده است، یک ایمیل تایید جدید ارسال شد.', + 'email_confirmation_awaiting' => 'آدرس ایمیل حساب مورد استفاده باید تایید شود', + 'ldap_fail_anonymous' => 'دسترسی LDAP با استفاده از صحافی ناشناس انجام نشد', + 'ldap_fail_authed' => 'دسترسی به LDAP با استفاده از جزئیات داده شده و رمز عبور انجام نشد', + 'ldap_extension_not_installed' => 'افزونه PHP LDAP نصب نشده است', + 'ldap_cannot_connect' => 'اتصال به سرور LDAP امکان پذیر نیست، اتصال اولیه برقرار نشد', + 'saml_already_logged_in' => 'قبلا وارد سیستم شده اید', + 'saml_user_not_registered' => 'کاربر :name ثبت نشده است و ثبت نام خودکار غیرفعال است', + 'saml_no_email_address' => 'آدرس داده ای برای این کاربر در داده های ارائه شده توسط سیستم احراز هویت خارجی یافت نشد', + 'saml_invalid_response_id' => 'درخواست از سیستم احراز هویت خارجی توسط فرایندی که توسط این نرم افزار آغاز شده است شناخته نمی شود. بازگشت به سیستم پس از ورود به سیستم می تواند باعث این مسئله شود.', + 'saml_fail_authed' => 'ورود به سیستم :system انجام نشد، سیستم مجوز موفقیت آمیز ارائه نکرد', + 'social_no_action_defined' => 'عملی تعریف نشده است', + 'social_login_bad_response' => "خطای دریافت شده در هنگام ورود به سیستم:\n:error", + 'social_account_in_use' => 'این حساب :socialAccount از قبل در حال استفاده است، سعی کنید از طریق گزینه :socialAccount وارد سیستم شوید.', + 'social_account_email_in_use' => 'ایمیل :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 به درستی پیکربندی نشده است.', 'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.', // System diff --git a/resources/lang/fa/pagination.php b/resources/lang/fa/pagination.php index 85bd12fc3..22fb0b89e 100644 --- a/resources/lang/fa/pagination.php +++ b/resources/lang/fa/pagination.php @@ -6,7 +6,7 @@ */ return [ - 'previous' => '« Previous', - 'next' => 'Next »', + 'previous' => '« قبلی', + 'next' => 'بعدی »', ]; diff --git a/resources/lang/fa/passwords.php b/resources/lang/fa/passwords.php index b408f3c2f..06b8f8b50 100644 --- a/resources/lang/fa/passwords.php +++ b/resources/lang/fa/passwords.php @@ -6,10 +6,10 @@ */ return [ - 'password' => 'Passwords must be at least eight characters and match the confirmation.', - 'user' => "We can't find a user with that e-mail address.", - 'token' => 'The password reset token is invalid for this email address.', - 'sent' => 'We have e-mailed your password reset link!', - 'reset' => 'Your password has been reset!', + 'password' => 'گذرواژه باید حداقل هشت حرف و با تایید مطابقت داشته باشد.', + 'user' => "ما کاربری با این نشانی ایمیل نداریم.", + 'token' => 'مشخصه‌ی بازگردانی رمز عبور معتبر نیست.', + 'sent' => 'لینک بازگردانی رمز عبور به ایمیل شما ارسال شد!', + 'reset' => 'رمز عبور شما بازگردانی شد!', ]; diff --git a/resources/lang/fa/settings.php b/resources/lang/fa/settings.php index 8a7946b12..4c1ae1345 100644 --- a/resources/lang/fa/settings.php +++ b/resources/lang/fa/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Role Details', 'role_name' => 'Role Name', 'role_desc' => 'Short Description of Role', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'System Permissions', 'role_manage_users' => 'Manage users', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Manage page templates', 'role_access_api' => 'Access system API', 'role_manage_settings' => 'Manage app settings', + 'role_export_content' => 'Export content', 'role_asset' => 'Asset Permissions', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Create Token', 'users_api_tokens_expires' => 'Expires', 'users_api_tokens_docs' => 'API Documentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/fa/validation.php b/resources/lang/fa/validation.php index 4031de2ae..cb9cd8eff 100644 --- a/resources/lang/fa/validation.php +++ b/resources/lang/fa/validation.php @@ -8,104 +8,106 @@ return [ // Standard laravel validation lines - 'accepted' => 'The :attribute must be accepted.', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'before' => 'The :attribute must be a date before :date.', + 'accepted' => ':attribute باید پذیرفته شده باشد.', + 'active_url' => 'آدرس :attribute معتبر نیست.', + 'after' => ':attribute باید تاریخی بعد از :date باشد.', + 'alpha' => ':attribute باید فقط حروف الفبا باشد.', + 'alpha_dash' => ':attribute باید فقط حروف الفبا، اعداد، خط تیره و زیرخط باشد.', + 'alpha_num' => ':attribute باید فقط حروف الفبا و اعداد باشد.', + 'array' => ':attribute باید آرایه باشد.', + 'backup_codes' => 'The provided code is not valid or has already been used.', + 'before' => ':attribute باید تاریخی قبل از :date باشد.', 'between' => [ - 'numeric' => 'The :attribute must be between :min and :max.', - 'file' => 'The :attribute must be between :min and :max kilobytes.', - 'string' => 'The :attribute must be between :min and :max characters.', - 'array' => 'The :attribute must have between :min and :max items.', + 'numeric' => ':attribute باید بین :min و :max باشد.', + 'file' => ':attribute باید بین :min و :max کیلوبایت باشد.', + 'string' => ':attribute باید بین :min و :max کاراکتر باشد.', + 'array' => ':attribute باید بین :min و :max آیتم باشد.', ], - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'email' => 'The :attribute must be a valid email address.', - 'ends_with' => 'The :attribute must end with one of the following: :values', - 'filled' => 'The :attribute field is required.', + 'boolean' => 'فیلد :attribute فقط می‌تواند true و یا false باشد.', + 'confirmed' => ':attribute با فیلد تکرار مطابقت ندارد.', + 'date' => ':attribute یک تاریخ معتبر نیست.', + 'date_format' => ':attribute با الگوی :format مطابقت ندارد.', + 'different' => ':attribute و :other باید از یکدیگر متفاوت باشند.', + 'digits' => ':attribute باید :digits رقم باشد.', + 'digits_between' => ':attribute باید بین :min و :max رقم باشد.', + 'email' => ':attribute باید یک ایمیل معتبر باشد.', + 'ends_with' => 'فیلد :attribute باید با یکی از مقادیر زیر خاتمه یابد: :values', + 'filled' => 'فیلد :attribute باید مقدار داشته باشد.', 'gt' => [ - 'numeric' => 'The :attribute must be greater than :value.', - 'file' => 'The :attribute must be greater than :value kilobytes.', - 'string' => 'The :attribute must be greater than :value characters.', - 'array' => 'The :attribute must have more than :value items.', + 'numeric' => ':attribute باید بزرگتر از :value باشد.', + 'file' => ':attribute باید بزرگتر از :value کیلوبایت باشد.', + 'string' => ':attribute باید بیشتر از :value کاراکتر داشته باشد.', + 'array' => ':attribute باید بیشتر از :value آیتم داشته باشد.', ], 'gte' => [ - 'numeric' => 'The :attribute must be greater than or equal :value.', - 'file' => 'The :attribute must be greater than or equal :value kilobytes.', - 'string' => 'The :attribute must be greater than or equal :value characters.', - 'array' => 'The :attribute must have :value items or more.', + 'numeric' => ':attribute باید بزرگتر یا مساوی :value باشد.', + 'file' => ':attribute باید بزرگتر یا مساوی :value کیلوبایت باشد.', + 'string' => ':attribute باید بیشتر یا مساوی :value کاراکتر داشته باشد.', + 'array' => ':attribute باید بیشتر یا مساوی :value آیتم داشته باشد.', ], - 'exists' => 'The selected :attribute is invalid.', - 'image' => 'The :attribute must be an image.', - 'image_extension' => 'The :attribute must have a valid & supported image extension.', - 'in' => 'The selected :attribute is invalid.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'ipv4' => 'The :attribute must be a valid IPv4 address.', - 'ipv6' => 'The :attribute must be a valid IPv6 address.', - 'json' => 'The :attribute must be a valid JSON string.', + 'exists' => ':attribute انتخاب شده، معتبر نیست.', + 'image' => ':attribute باید یک تصویر معتبر باشد.', + 'image_extension' => ':attribute باید یک تصویر با فرمت معتبر باشد.', + 'in' => ':attribute انتخاب شده، معتبر نیست.', + 'integer' => ':attribute باید عدد صحیح باشد.', + 'ip' => ':attribute باید آدرس IP معتبر باشد.', + 'ipv4' => ':attribute باید یک آدرس معتبر از نوع IPv4 باشد.', + 'ipv6' => ':attribute باید یک آدرس معتبر از نوع IPv6 باشد.', + 'json' => 'فیلد :attribute باید یک رشته از نوع JSON باشد.', 'lt' => [ - 'numeric' => 'The :attribute must be less than :value.', - 'file' => 'The :attribute must be less than :value kilobytes.', - 'string' => 'The :attribute must be less than :value characters.', - 'array' => 'The :attribute must have less than :value items.', + 'numeric' => ':attribute باید کوچکتر از :value باشد.', + 'file' => ':attribute باید کوچکتر از :value کیلوبایت باشد.', + 'string' => ':attribute باید کمتر از :value کاراکتر داشته باشد.', + 'array' => ':attribute باید کمتر از :value آیتم داشته باشد.', ], 'lte' => [ - 'numeric' => 'The :attribute must be less than or equal :value.', - 'file' => 'The :attribute must be less than or equal :value kilobytes.', - 'string' => 'The :attribute must be less than or equal :value characters.', - 'array' => 'The :attribute must not have more than :value items.', + 'numeric' => ':attribute باید کوچکتر یا مساوی :value باشد.', + 'file' => ':attribute باید کوچکتر یا مساوی :value کیلوبایت باشد.', + 'string' => ':attribute باید کمتر یا مساوی :value کاراکتر داشته باشد.', + 'array' => ':attribute باید کمتر یا مساوی :value آیتم داشته باشد.', ], 'max' => [ - 'numeric' => 'The :attribute may not be greater than :max.', - 'file' => 'The :attribute may not be greater than :max kilobytes.', - 'string' => 'The :attribute may not be greater than :max characters.', - 'array' => 'The :attribute may not have more than :max items.', + 'numeric' => ':attribute نباید بزرگتر از :max باشد.', + 'file' => ':attribute نباید بزرگتر از :max کیلوبایت باشد.', + 'string' => ':attribute نباید بیشتر از :max کاراکتر داشته باشد.', + 'array' => ':attribute نباید بیشتر از :max آیتم داشته باشد.', ], - 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimes' => 'فرمت‌های معتبر فایل عبارتند از: :values.', 'min' => [ - 'numeric' => 'The :attribute must be at least :min.', - 'file' => 'The :attribute must be at least :min kilobytes.', - 'string' => 'The :attribute must be at least :min characters.', - 'array' => 'The :attribute must have at least :min items.', + 'numeric' => ':attribute نباید کوچکتر از :min باشد.', + 'file' => ':attribute نباید کوچکتر از :min کیلوبایت باشد.', + 'string' => ':attribute نباید کمتر از :min کاراکتر داشته باشد.', + 'array' => ':attribute نباید کمتر از :min آیتم داشته باشد.', ], - 'not_in' => 'The selected :attribute is invalid.', - 'not_regex' => 'The :attribute format is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values is present.', - 'required_without' => 'The :attribute field is required when :values is not present.', - 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'safe_url' => 'The provided link may not be safe.', + 'not_in' => ':attribute انتخاب شده، معتبر نیست.', + 'not_regex' => 'فرمت :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 باید همانند هم باشند.', + 'safe_url' => ':attribute معتبر نمی‌باشد.', 'size' => [ - 'numeric' => 'The :attribute must be :size.', - 'file' => 'The :attribute must be :size kilobytes.', - 'string' => 'The :attribute must be :size characters.', - 'array' => 'The :attribute must contain :size items.', + 'numeric' => ':attribute باید برابر با :size باشد.', + 'file' => ':attribute باید برابر با :size کیلوبایت باشد.', + 'string' => ':attribute باید برابر با :size کاراکتر باشد.', + 'array' => ':attribute باید شامل :size آیتم باشد.', ], - 'string' => 'The :attribute must be a string.', - 'timezone' => 'The :attribute must be a valid zone.', - 'unique' => 'The :attribute has already been taken.', - 'url' => 'The :attribute format is invalid.', - 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', + 'string' => 'فیلد :attribute باید متن باشد.', + 'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.', + 'totp' => 'The provided code is not valid or has expired.', + 'unique' => ':attribute قبلا انتخاب شده است.', + 'url' => ':attribute معتبر نمی‌باشد.', + 'uploaded' => 'بارگذاری فایل :attribute موفقیت آمیز نبود.', // Custom validation lines 'custom' => [ 'password-confirm' => [ - 'required_with' => 'Password confirmation required', + 'required_with' => 'تایید کلمه عبور اجباری می باشد', ], ], diff --git a/resources/lang/fr/activities.php b/resources/lang/fr/activities.php index 05f741009..c1db4a644 100644 --- a/resources/lang/fr/activities.php +++ b/resources/lang/fr/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" a été ajouté dans vos favoris', 'favourite_remove_notification' => '":name" a été supprimé de vos favoris', + // MFA + 'mfa_setup_method_notification' => 'Méthode multi-facteurs configurée avec succès', + 'mfa_remove_method_notification' => 'Méthode multi-facteurs supprimée avec succès', + // Other 'commented_on' => 'a commenté', 'permissions_update' => 'mettre à jour les autorisations', diff --git a/resources/lang/fr/auth.php b/resources/lang/fr/auth.php index 07252420a..0e5e9b0d7 100644 --- a/resources/lang/fr/auth.php +++ b/resources/lang/fr/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Bienvenue dans :appName !', 'user_invite_page_text' => 'Pour finaliser votre compte et recevoir l\'accès, vous devez renseigner le mot de passe qui sera utilisé pour la connexion à :appName les prochaines fois.', 'user_invite_page_confirm_button' => 'Confirmez le mot de passe', - 'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !' + 'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !', + + // Multi-factor Authentication + 'mfa_setup' => 'Configuration authentification multi-facteurs', + 'mfa_setup_desc' => 'Configurez l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', + 'mfa_setup_configured' => 'Déjà configuré', + 'mfa_setup_reconfigure' => 'Reconfigurer', + 'mfa_setup_remove_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette méthode d\'authentification multi-facteurs ?', + 'mfa_setup_action' => 'Configuration', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Application mobile', + 'mfa_option_totp_desc' => 'Pour utiliser l\'authentification multi-facteurs, vous aurez besoin d\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirmer et activer', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Stockez la liste des codes ci-dessous dans un endroit sûr. Lorsque vous accédez au système, vous pourrez utiliser l\'un des codes comme un deuxième mécanisme d\'authentification.', + 'mfa_gen_backup_codes_download' => 'Télécharger le code', + 'mfa_gen_backup_codes_usage_warning' => 'Chaque code ne peut être utilisé qu\'une seule fois', + 'mfa_gen_totp_title' => 'Configuration de l\'application mobile', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Vérifier la configuration', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Fournir le code généré par votre application ici', + 'mfa_verify_access' => 'Vérifier l\'accès', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'Aucune méthode configurée', + 'mfa_verify_no_methods_desc' => 'Aucune méthode d\'authentification multi-facteurs n\'a pu être trouvée pour votre compte. Vous devez configurer au moins une méthode avant d\'obtenir l\'accès.', + 'mfa_verify_use_totp' => 'Vérifier à l\'aide d\'une application mobile', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Entrez ci-dessous le code généré à l\'aide de votre application mobile :', + 'mfa_setup_login_notification' => 'Méthode multi-facteurs configurée. Veuillez maintenant vous reconnecter en utilisant la méthode configurée.', ]; \ No newline at end of file diff --git a/resources/lang/fr/common.php b/resources/lang/fr/common.php index 6203c7326..b25a817ab 100644 --- a/resources/lang/fr/common.php +++ b/resources/lang/fr/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Réinitialiser', 'remove' => 'Enlever', 'add' => 'Ajouter', + 'configure' => 'Configurer', 'fullscreen' => 'Plein écran', 'favourite' => 'Favoris', 'unfavourite' => 'Supprimer des favoris', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Aucune activité', 'no_items' => 'Aucun élément', 'back_to_top' => 'Retour en haut', + 'skip_to_main_content' => 'Passer au contenu principal', 'toggle_details' => 'Afficher les détails', 'toggle_thumbnails' => 'Afficher les vignettes', 'details' => 'Détails', diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php index 507d630e1..f30dd8b8f 100644 --- a/resources/lang/fr/entities.php +++ b/resources/lang/fr/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Fichiers web', 'export_pdf' => 'Fichier PDF', 'export_text' => 'Document texte', + 'export_md' => 'Fichiers Markdown', // Permissions and restrictions 'permissions' => 'Autorisations', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permissions de l\'étagère', 'shelves_permissions_updated' => 'Permissions de l\'étagère mises à jour', 'shelves_permissions_active' => 'Permissions de l\'étagère activées', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copier les permissions vers les livres', 'shelves_copy_permissions' => 'Copier les permissions', 'shelves_copy_permissions_explain' => 'Ceci va appliquer les permissions actuelles de cette étagère à tous les livres qu\'elle contient. Avant de continuer, assurez-vous que toutes les permissions de cette étagère ont été sauvegardées.', diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php index ab1febc65..56f18d5fc 100644 --- a/resources/lang/fr/settings.php +++ b/resources/lang/fr/settings.php @@ -25,12 +25,12 @@ return [ 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?', 'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisé', 'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.', - 'app_editor' => 'Editeur des pages', + 'app_editor' => 'Éditeur des pages', 'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.', 'app_custom_html' => 'HTML personnalisé dans l\'en-tête', 'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.', 'app_custom_html_disabled_notice' => 'Le contenu de la tête HTML personnalisée est désactivé sur cette page de paramètres pour garantir que les modifications les plus récentes peuvent être annulées.', - 'app_logo' => 'Logo de l\'Application', + 'app_logo' => 'Logo de l\'application', 'app_logo_desc' => 'Cette image doit faire 43px de hauteur.
      Les images plus larges seront réduites.', 'app_primary_color' => 'Couleur principale de l\'application', 'app_primary_color_desc' => 'Cela devrait être une valeur hexadécimale.
      Laisser vide pour rétablir la couleur par défaut.', @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Corbeille', 'recycle_bin_desc' => 'Ici, vous pouvez restaurer les éléments qui ont été supprimés ou choisir de les effacer définitivement du système. Cette liste n\'est pas filtrée contrairement aux listes d\'activités similaires dans le système pour lesquelles les filtres d\'autorisation sont appliqués.', 'recycle_bin_deleted_item' => 'Élément supprimé', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Supprimé par', 'recycle_bin_deleted_at' => 'Date de suppression', 'recycle_bin_permanently_delete' => 'Supprimer définitivement', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Éléments à restaurer', 'recycle_bin_restore_confirm' => 'Cette action restaurera l\'élément supprimé, y compris tous les éléments enfants, à leur emplacement d\'origine. Si l\'emplacement d\'origine a été supprimé depuis et est maintenant dans la corbeille, l\'élément parent devra également être restauré.', 'recycle_bin_restore_deleted_parent' => 'Le parent de cet élément a également été supprimé. Ceux-ci resteront supprimés jusqu\'à ce que ce parent soit également restauré.', + 'recycle_bin_restore_parent' => 'Restaurer le parent', 'recycle_bin_destroy_notification' => ':count éléments totaux supprimés de la corbeille.', 'recycle_bin_restore_notification' => ':count éléments totaux restaurés de la corbeille.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Détails du rôle', 'role_name' => 'Nom du rôle', 'role_desc' => 'Courte description du rôle', + 'role_mfa_enforced' => 'Nécessite une authentification multi-facteurs', 'role_external_auth_id' => 'Identifiants d\'authentification externes', 'role_system' => 'Permissions système', 'role_manage_users' => 'Gérer les utilisateurs', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Gérer les modèles de page', 'role_access_api' => 'Accès à l\'API du système', 'role_manage_settings' => 'Gérer les préférences de l\'application', + 'role_export_content' => 'Export content', 'role_asset' => 'Permissions des ressources', 'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. Attribuer uniquement des rôles avec ces permissions à des utilisateurs de confiance.', 'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Créer un jeton', 'users_api_tokens_expires' => 'Expiré', 'users_api_tokens_docs' => 'Documentation de l\'API', + 'users_mfa' => 'Authentification multi-facteurs', + 'users_mfa_desc' => 'Configurez l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Méthode de configuration', // API Tokens 'user_api_token_create' => 'Créer un nouveau jeton API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norvegien', diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 684404f42..ed1964f06 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute doit contenir uniquement des lettres, chiffres et traits d\'union.', 'alpha_num' => ':attribute doit contenir uniquement des chiffres et des lettres.', 'array' => ':attribute doit être un tableau.', + 'backup_codes' => 'Le code fourni n\'est pas valide ou a déjà été utilisé.', 'before' => ':attribute doit être inférieur à :date.', 'between' => [ 'numeric' => ':attribute doit être compris entre :min et :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute doit être une chaîne de caractères.', 'timezone' => ':attribute doit être une zone valide.', + 'totp' => 'Le code fourni n\'est pas valide ou est expiré.', 'unique' => ':attribute est déjà utilisé.', 'url' => ':attribute a un format invalide.', 'uploaded' => 'Le fichier n\'a pas pu être envoyé. Le serveur peut ne pas accepter des fichiers de cette taille.', diff --git a/resources/lang/he/activities.php b/resources/lang/he/activities.php index dfa543442..c19825afe 100644 --- a/resources/lang/he/activities.php +++ b/resources/lang/he/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'commented on', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/he/auth.php b/resources/lang/he/auth.php index 733c84f9d..a078cfcf8 100644 --- a/resources/lang/he/auth.php +++ b/resources/lang/he/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Welcome to :appName!', 'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.', 'user_invite_page_confirm_button' => 'Confirm Password', - 'user_invite_success' => 'Password set, you now have access to :appName!' + 'user_invite_success' => 'Password set, you now have access to :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/he/common.php b/resources/lang/he/common.php index 3ab175bae..475987f34 100644 --- a/resources/lang/he/common.php +++ b/resources/lang/he/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'איפוס', 'remove' => 'הסר', 'add' => 'הוסף', + 'configure' => 'Configure', 'fullscreen' => 'Fullscreen', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'אין פעילות להציג', 'no_items' => 'אין פריטים זמינים', 'back_to_top' => 'בחזרה ללמעלה', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'הצג/הסתר פרטים', 'toggle_thumbnails' => 'הצג/הסתר תמונות', 'details' => 'פרטים', diff --git a/resources/lang/he/entities.php b/resources/lang/he/entities.php index a0b4918ae..23aa50158 100644 --- a/resources/lang/he/entities.php +++ b/resources/lang/he/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'דף אינטרנט', 'export_pdf' => 'קובץ PDF', 'export_text' => 'טקסט רגיל', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'הרשאות', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'הרשאות מדף', 'shelves_permissions_updated' => 'הרשאות מדף עודכנו', 'shelves_permissions_active' => 'הרשאות מדף פעילות', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'העתק הרשאות מדף אל הספרים', 'shelves_copy_permissions' => 'העתק הרשאות', 'shelves_copy_permissions_explain' => 'פעולה זו תעתיק את כל הרשאות המדף לכל הספרים המשוייכים למדף זה. לפני הביצוע, יש לוודא שכל הרשאות המדף אכן נשמרו.', diff --git a/resources/lang/he/settings.php b/resources/lang/he/settings.php index a648e2e02..c3b37eecf 100755 --- a/resources/lang/he/settings.php +++ b/resources/lang/he/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'סל המיחזור', 'recycle_bin_desc' => 'כאן תוכלו לאחזר פריטים שנמחקו או לבחור למחוק אותם מהמערכת לצמיתות. רשימה זו לא מסוננת, בשונה מרשימות פעילות דומות במערכת, בהן מוחלים מסנני הרשאות.', 'recycle_bin_deleted_item' => 'פריט שנמחק', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'נמחק על ידי', 'recycle_bin_deleted_at' => 'זמן המחיקה', 'recycle_bin_permanently_delete' => 'מחק לצמיתות', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'פריטים שיאוחזרו', 'recycle_bin_restore_confirm' => 'פעולה זו תאחזר את הפריט שנמחק, לרבות רכיבי-הבן שלו, למיקומו המקורי. אם המיקום המקורי נמחק מאז, וכעת נמצא בסל המיחזור, יש לאחזר גם את פריט-האב.', 'recycle_bin_restore_deleted_parent' => 'פריט-האב של פריט זה נמחק. פריטים אלה יישארו מחוקים עד שפריט-אב זה יאוחזר.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'נמחקו בסה"כ :count פריטים מסל המיחזור.', 'recycle_bin_restore_notification' => 'אוחזרו בסה"כ :count פריטים מסל המיחזור.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'פרטי תפקיד', 'role_name' => 'שם התפקיד', 'role_desc' => 'תיאור קצר של התפקיד', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'ID-י אותנטיקציה חיצוניים', 'role_system' => 'הרשאות מערכת', 'role_manage_users' => 'ניהול משתמשים', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'נהל תבניות דפים', 'role_access_api' => 'גש ל-API המערכת', 'role_manage_settings' => 'ניהול הגדרות יישום', + 'role_export_content' => 'Export content', 'role_asset' => 'הרשאות משאבים', 'roles_system_warning' => 'שימו לב לכך שגישה לכל אחת משלושת ההרשאות הנ"ל יכולה לאפשר למשתמש לשנות את הפריווילגיות שלהם או של אחרים במערכת. הגדירו תפקידים להרשאות אלה למשתמשים בהם אתם בוטחים בלבד.', 'role_asset_desc' => 'הרשאות אלו שולטות בגישת ברירת המחדל למשאבים בתוך המערכת. הרשאות של ספרים, פרקים ודפים יגברו על הרשאות אלו.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'צור אסימון', 'users_api_tokens_expires' => 'פג', 'users_api_tokens_docs' => 'תיעוד API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'צור אסימון API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/he/validation.php b/resources/lang/he/validation.php index 8a18ca27a..e52c28b42 100644 --- a/resources/lang/he/validation.php +++ b/resources/lang/he/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'שדה :attribute יכול להכיל אותיות, מספרים ומקפים בלבד.', 'alpha_num' => 'שדה :attribute יכול להכיל אותיות ומספרים בלבד.', 'array' => 'שדה :attribute חייב להיות מערך.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'שדה :attribute חייב להיות תאריך לפני :date.', 'between' => [ 'numeric' => 'שדה :attribute חייב להיות בין :min ל-:max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'שדה :attribute חייב להיות מחרוזת.', 'timezone' => 'שדה :attribute חייב להיות איזור תקני.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'שדה :attribute כבר תפוס.', 'url' => 'שדה :attribute בעל פורמט שאינו תקין.', 'uploaded' => 'שדה :attribute ארעה שגיאה בעת ההעלאה.', diff --git a/resources/lang/hr/activities.php b/resources/lang/hr/activities.php index a3340b4dd..6609f552c 100644 --- a/resources/lang/hr/activities.php +++ b/resources/lang/hr/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'komentirano', 'permissions_update' => 'ažurirana dopuštenja', diff --git a/resources/lang/hr/auth.php b/resources/lang/hr/auth.php index 9680cc700..23014439a 100644 --- a/resources/lang/hr/auth.php +++ b/resources/lang/hr/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Dobrodošli u :appName!', 'user_invite_page_text' => 'Da biste postavili račun i dobili pristup trebate unijeti lozinku kojom ćete se ubuduće prijaviti na :appName.', 'user_invite_page_confirm_button' => 'Potvrdite lozinku', - 'user_invite_success' => 'Lozinka je postavljena, možete pristupiti :appName!' + 'user_invite_success' => 'Lozinka je postavljena, možete pristupiti :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/hr/common.php b/resources/lang/hr/common.php index 1df3c227c..bc51eed22 100644 --- a/resources/lang/hr/common.php +++ b/resources/lang/hr/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Ponovno postavi', 'remove' => 'Ukloni', 'add' => 'Dodaj', + 'configure' => 'Configure', 'fullscreen' => 'Cijeli zaslon', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nema aktivnosti za pregled', 'no_items' => 'Nedostupno', 'back_to_top' => 'Natrag na vrh', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Prebaci detalje', 'toggle_thumbnails' => 'Uključi minijature', 'details' => 'Detalji', diff --git a/resources/lang/hr/entities.php b/resources/lang/hr/entities.php index e8a8004e2..4bef3814f 100644 --- a/resources/lang/hr/entities.php +++ b/resources/lang/hr/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Web File', 'export_pdf' => 'PDF File', 'export_text' => 'Text File', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Dopuštenja', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Dopuštenja za policu', 'shelves_permissions_updated' => 'Ažurirana dopuštenja za policu', 'shelves_permissions_active' => 'Aktivirana dopuštenja za policu', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopiraj dopuštenja za knjige', 'shelves_copy_permissions' => 'Kopiraj dopuštenja', 'shelves_copy_permissions_explain' => 'Ovo će promijeniti trenutna dopuštenja za policu i knjige u njoj. Prije aktivacije provjerite jesu li sve dopuštenja za ovu policu spremljena.', diff --git a/resources/lang/hr/settings.php b/resources/lang/hr/settings.php index 9c4d6ebe4..eaed1a577 100644 --- a/resources/lang/hr/settings.php +++ b/resources/lang/hr/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Ovdje možete vratiti izbrisane stavke ili ih trajno ukloniti iz sustava. Popis nije filtriran kao što su to popisi u kojima su omogućeni filteri.', 'recycle_bin_deleted_item' => 'Izbrisane stavke', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Izbrisano od', 'recycle_bin_deleted_at' => 'Vrijeme brisanja', 'recycle_bin_permanently_delete' => 'Trajno izbrisano', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Stavke koje treba vratiti', 'recycle_bin_restore_confirm' => 'Ova radnja vraća izbrisane stavke i njene podređene elemente na prvobitnu lokaciju. Ako je nadređena stavka izbrisana i nju treba vratiti.', 'recycle_bin_restore_deleted_parent' => 'S obzirom da je nadređena stavka obrisana najprije treba vratiti nju.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Ukupno izbrisane :count stavke iz Recycle Bin', 'recycle_bin_restore_notification' => 'Ukupno vraćene :count stavke iz Recycle Bin', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detalji uloge', 'role_name' => 'Ime uloge', 'role_desc' => 'Kratki opis uloge', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Autorizacija', 'role_system' => 'Dopuštenja sustava', 'role_manage_users' => 'Upravljanje korisnicima', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Upravljanje predlošcima stranica', 'role_access_api' => 'API pristup', 'role_manage_settings' => 'Upravljanje postavkama aplikacija', + 'role_export_content' => 'Export content', 'role_asset' => 'Upravljanje vlasništvom', 'roles_system_warning' => 'Uzmite u obzir da pristup bilo kojem od ovih dopuštenja dozvoljavate korisniku upravljanje dopuštenjima ostalih u sustavu. Ova dopuštenja dodijelite pouzdanim korisnicima.', 'role_asset_desc' => 'Ova dopuštenja kontroliraju zadane pristupe. Dopuštenja za knjige, poglavlja i stranice ih poništavaju.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Stvori token', 'users_api_tokens_expires' => 'Isteklo', 'users_api_tokens_docs' => 'API dokumentacija', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Stvori API token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/hr/validation.php b/resources/lang/hr/validation.php index 5b1849c5f..b88e872f1 100644 --- a/resources/lang/hr/validation.php +++ b/resources/lang/hr/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute može sadržavati samo slova, brojeve, crtice i donje crtice.', 'alpha_num' => ':attribute može sadržavati samo slova i brojeve.', 'array' => ':attribute mora biti niz.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute mora biti prije :date.', 'between' => [ 'numeric' => ':attribute mora biti između :min i :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute mora biti niz.', 'timezone' => ':attribute mora biti valjan.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute se već koristi.', 'url' => 'Format :attribute nije valjan.', 'uploaded' => 'Datoteka se ne može prenijeti. Server možda ne prihvaća datoteke te veličine.', diff --git a/resources/lang/hu/activities.php b/resources/lang/hu/activities.php index ff2ddb490..98bdd798b 100644 --- a/resources/lang/hu/activities.php +++ b/resources/lang/hu/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'megjegyzést fűzött hozzá:', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/hu/auth.php b/resources/lang/hu/auth.php index a13c8300e..7aaedf92d 100644 --- a/resources/lang/hu/auth.php +++ b/resources/lang/hu/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => ':appName üdvözöl!', 'user_invite_page_text' => 'A fiók véglegesítéséhez és a hozzáféréshez be kell állítani egy jelszót ami :appName weboldalon lesz használva a bejelentkezéshez.', 'user_invite_page_confirm_button' => 'Jelszó megerősítése', - 'user_invite_success' => 'Jelszó beállítva, :appName most már elérhető!' + 'user_invite_success' => 'Jelszó beállítva, :appName most már elérhető!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/hu/common.php b/resources/lang/hu/common.php index 948bbaefd..2e850aa2e 100644 --- a/resources/lang/hu/common.php +++ b/resources/lang/hu/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Visszaállítás', 'remove' => 'Eltávolítás', 'add' => 'Hozzáadás', + 'configure' => 'Configure', 'fullscreen' => 'Teljes képernyő', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nincs megjeleníthető aktivitás', 'no_items' => 'Nincsenek elérhető elemek', 'back_to_top' => 'Oldal eleje', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Részletek átkapcsolása', 'toggle_thumbnails' => 'Bélyegképek átkapcsolása', 'details' => 'Részletek', diff --git a/resources/lang/hu/entities.php b/resources/lang/hu/entities.php index 4d789bfd0..6050416e5 100644 --- a/resources/lang/hu/entities.php +++ b/resources/lang/hu/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Webfájlt tartalmaz', 'export_pdf' => 'PDF fájl', 'export_text' => 'Egyszerű szövegfájl', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Jogosultságok', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Könyvespolc jogosultság', 'shelves_permissions_updated' => 'Könyvespolc jogosultságok frissítve', 'shelves_permissions_active' => 'Könyvespolc jogosultságok aktívak', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Jogosultság másolása könyvekre', 'shelves_copy_permissions' => 'Jogosultság másolása', 'shelves_copy_permissions_explain' => 'Ez alkalmazni fogja ennek a könyvespolcnak az aktuális jogosultság beállításait az összes benne található könyvön. Aktiválás előtt ellenőrizni kell, hogy a könyvespolc jogosultságain végzett összes módosítás el lett mentve.', diff --git a/resources/lang/hu/settings.php b/resources/lang/hu/settings.php index d8f052008..ef8c506c0 100644 --- a/resources/lang/hu/settings.php +++ b/resources/lang/hu/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Lomtár', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Törölt elem', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Törölte', 'recycle_bin_deleted_at' => 'Törlés ideje', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Szerepkör részletei', 'role_name' => 'Szerepkör neve', 'role_desc' => 'Szerepkör rövid leírása', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Külső hitelesítés azonosítók', 'role_system' => 'Rendszer jogosultságok', 'role_manage_users' => 'Felhasználók kezelése', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Oldalsablonok kezelése', 'role_access_api' => 'Hozzáférés a rendszer API-hoz', 'role_manage_settings' => 'Alkalmazás beállításainak kezelése', + 'role_export_content' => 'Export content', 'role_asset' => 'Eszköz jogosultságok', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'Ezek a jogosultság vezérlik a alapértelmezés szerinti hozzáférést a rendszerben található eszközökhöz. A könyvek, fejezetek és oldalak jogosultságai felülírják ezeket a jogosultságokat.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Vezérjel létrehozása', 'users_api_tokens_expires' => 'Lejárat', 'users_api_tokens_docs' => 'API dokumentáció', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'API vezérjel létrehozása', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/hu/validation.php b/resources/lang/hu/validation.php index d82e32632..3d24cec3c 100644 --- a/resources/lang/hu/validation.php +++ b/resources/lang/hu/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute csak betűket, számokat és kötőjeleket tartalmazhat.', 'alpha_num' => ':attribute csak betűket és számokat tartalmazhat.', 'array' => ':attribute tömb kell legyen.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute dátumnak :date előttinek kell lennie.', 'between' => [ 'numeric' => ':attribute értékének :min és :max között kell lennie.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute karaktersorozatnak kell legyen.', 'timezone' => ':attribute érvényes zóna kell legyen.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute már elkészült.', 'url' => ':attribute formátuma érvénytelen.', 'uploaded' => 'A fájlt nem lehet feltölteni. A kiszolgáló nem fogad el ilyen méretű fájlokat.', diff --git a/resources/lang/id/activities.php b/resources/lang/id/activities.php index 1255e32dc..bac965be4 100644 --- a/resources/lang/id/activities.php +++ b/resources/lang/id/activities.php @@ -8,8 +8,8 @@ return [ // Pages 'page_create' => 'telah membuat halaman', 'page_create_notification' => 'Halaman Berhasil dibuat', - 'page_update' => 'halaman diperbaharui', - 'page_update_notification' => 'Halaman Berhasil Diperbarui', + 'page_update' => 'halaman telah diperbaharui', + 'page_update_notification' => 'Berhasil mengupdate halaman', 'page_delete' => 'halaman dihapus', 'page_delete_notification' => 'Berhasil menghapus halaman', 'page_restore' => 'halaman telah dipulihkan', @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" telah ditambahkan ke favorit Anda', 'favourite_remove_notification' => '":name" telah dihapus dari favorit Anda', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'berkomentar pada', 'permissions_update' => 'izin diperbarui', diff --git a/resources/lang/id/auth.php b/resources/lang/id/auth.php index 0fc965dae..d800ebbcc 100644 --- a/resources/lang/id/auth.php +++ b/resources/lang/id/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Selamat datang di :appName!', 'user_invite_page_text' => 'Untuk menyelesaikan akun Anda dan mendapatkan akses, Anda perlu mengatur kata sandi yang akan digunakan untuk masuk ke :appName pada kunjungan berikutnya.', 'user_invite_page_confirm_button' => 'Konfirmasi Kata sandi', - 'user_invite_success' => 'Atur kata sandi, Anda sekarang memiliki akses ke :appName!' + 'user_invite_success' => 'Atur kata sandi, Anda sekarang memiliki akses ke :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/id/common.php b/resources/lang/id/common.php index 893ff7603..c9aac2ed2 100644 --- a/resources/lang/id/common.php +++ b/resources/lang/id/common.php @@ -5,89 +5,91 @@ return [ // Buttons - 'cancel' => 'Batalkan', + 'cancel' => 'Batal', 'confirm' => 'Konfirmasi', 'back' => 'Kembali', 'save' => 'Simpan', - 'continue' => 'Selanjutnya', + 'continue' => 'Lanjutkan', 'select' => 'Pilih', - 'toggle_all' => 'Alihkan semua', - 'more' => 'Lebih', + 'toggle_all' => 'Alihkan Semua', + 'more' => 'Lebih banyak', // Form Labels 'name' => 'Nama', 'description' => 'Deskripsi', - 'role' => 'Wewenang', - 'cover_image' => 'Sampul Gambar', + 'role' => 'Peran', + 'cover_image' => 'Sampul gambar', 'cover_image_description' => 'Gambar ini harus berukuran kira-kira 440x250 piksel.', // Actions 'actions' => 'Tindakan', - 'view' => 'Melihat', + 'view' => 'Lihat', 'view_all' => 'Lihat Semua', 'create' => 'Buat', - 'update' => 'Perbaharui', + 'update' => 'Perbarui', 'edit' => 'Sunting', 'sort' => 'Sortir', 'move' => 'Pindahkan', 'copy' => 'Salin', - 'reply' => 'Balasan', + 'reply' => 'Balas', 'delete' => 'Hapus', 'delete_confirm' => 'Konfirmasi Penghapusan', 'search' => 'Cari', 'search_clear' => 'Hapus Pencarian', - 'reset' => 'Setel Ulang', + 'reset' => 'Atur ulang', 'remove' => 'Hapus', 'add' => 'Tambah', + 'configure' => 'Configure', 'fullscreen' => 'Layar Penuh', 'favourite' => 'Favorit', - 'unfavourite' => 'Tidak favorit', - 'next' => 'Lanjut', + 'unfavourite' => 'Batal favorit', + 'next' => 'Selanjutnya', 'previous' => 'Sebelumnya', // Sort Options - 'sort_options' => 'Sortir Pilihan', - 'sort_direction_toggle' => 'Urutkan Arah Toggle', - 'sort_ascending' => 'Sortir Naik', - 'sort_descending' => 'Urutkan Menurun', + 'sort_options' => 'Opsi Sortir', + 'sort_direction_toggle' => 'Urutkan Arah Alihan', + 'sort_ascending' => 'Sortir Menaik', + 'sort_descending' => 'Sortir Menurun', 'sort_name' => 'Nama', 'sort_default' => 'Bawaan', - 'sort_created_at' => 'Tanggal dibuat', - 'sort_updated_at' => 'Tanggal diperbaharui', + 'sort_created_at' => 'Tanggal Dibuat', + 'sort_updated_at' => 'Tanggal Diperbarui', // Misc - 'deleted_user' => 'Pengguna terhapus', - 'no_activity' => 'Tidak ada aktifitas untuk ditampilkan', + 'deleted_user' => 'Pengguna yang Dihapus', + 'no_activity' => 'Tidak ada aktivitas untuk ditampilkan', 'no_items' => 'Tidak ada item yang tersedia', 'back_to_top' => 'Kembali ke atas', - 'toggle_details' => 'Detail Toggle', + 'skip_to_main_content' => 'Lewatkan ke konten utama', + 'toggle_details' => 'Rincian Alihan', 'toggle_thumbnails' => 'Alihkan Gambar Mini', - 'details' => 'Detail', - 'grid_view' => 'Tampilan bergaris', - 'list_view' => 'Daftar Tampilan', - 'default' => 'Default', + 'details' => 'Rincian', + 'grid_view' => 'Tampilan Bergaris', + 'list_view' => 'Tampilan Daftar', + 'default' => 'Bawaan', 'breadcrumb' => 'Breadcrumb', // Header 'header_menu_expand' => 'Perluas Menu Tajuk', - 'profile_menu' => 'Profile Menu', - 'view_profile' => 'Tampilkan profil', + 'profile_menu' => 'Menu Profil', + 'view_profile' => 'Tampilkan Profil', 'edit_profile' => 'Sunting Profil', 'dark_mode' => 'Mode Gelap', - 'light_mode' => 'Mode Cahaya', + 'light_mode' => 'Mode Terang', // Layout tabs 'tab_info' => 'Informasi', - 'tab_info_label' => 'Tab Menampilkan Informasi Sekunder', + 'tab_info_label' => 'Tab: Tampilkan Informasi Sekunder', 'tab_content' => 'Konten', - 'tab_content_label' => 'Tab Menampilkan Informasi Utama', + 'tab_content_label' => 'Tab: Tampilkan Informasi Utama', // Email Content - 'email_action_help' => 'Jika Anda mengalami masalah saat mengklik tombol ":actionText", salin dan tempel URL di bawah ini ke browser web Anda:', - 'email_rights' => 'Seluruh hak cipta', + 'email_action_help' => 'Jika Anda mengalami masalah saat mengklik tombol ":actionText", salin dan tempel URL di bawah ke dalam peramban web Anda:', + 'email_rights' => 'Hak cipta dilindungi', // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Rahasia pribadi', - 'terms_of_service' => 'Persyaratan Layanan', + 'privacy_policy' => 'Kebijakan Privasi', + 'terms_of_service' => 'Ketentuan Layanan', ]; diff --git a/resources/lang/id/entities.php b/resources/lang/id/entities.php index 5f6afb807..aa92fd06f 100644 --- a/resources/lang/id/entities.php +++ b/resources/lang/id/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'File Web Berisi', 'export_pdf' => 'Dokumen PDF', 'export_text' => 'Dokumen Teks Biasa', + 'export_md' => 'File Markdown', // Permissions and restrictions 'permissions' => 'Izin', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Izin Rak Buku', 'shelves_permissions_updated' => 'Izin Rak Buku Diperbarui', 'shelves_permissions_active' => 'Izin Rak Buku Aktif', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Salin Izin ke Buku', 'shelves_copy_permissions' => 'Salin Izin', 'shelves_copy_permissions_explain' => 'Ini akan menerapkan setelan izin rak buku ini saat ini ke semua buku yang ada di dalamnya. Sebelum mengaktifkan, pastikan setiap perubahan pada izin rak buku ini telah disimpan.', diff --git a/resources/lang/id/settings.php b/resources/lang/id/settings.php index 80e8d38e1..105377e0c 100644 --- a/resources/lang/id/settings.php +++ b/resources/lang/id/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Tempat Sampah', 'recycle_bin_desc' => 'Di sini Anda dapat memulihkan item yang telah dihapus atau memilih untuk menghapusnya secara permanen dari sistem. Daftar ini tidak difilter, tidak seperti daftar aktivitas serupa di sistem tempat filter izin diterapkan.', 'recycle_bin_deleted_item' => 'Item yang Dihapus', + 'recycle_bin_deleted_parent' => 'Induk', 'recycle_bin_deleted_by' => 'Dihapus Oleh', 'recycle_bin_deleted_at' => 'Waktu Penghapusan', 'recycle_bin_permanently_delete' => 'Hapus Permanen', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Item yang akan Dipulihkan', 'recycle_bin_restore_confirm' => 'Tindakan ini akan memulihkan item yang dihapus, termasuk semua elemen anak, ke lokasi aslinya. Jika lokasi asli telah dihapus, dan sekarang berada di keranjang sampah, item induk juga perlu dipulihkan.', 'recycle_bin_restore_deleted_parent' => 'Induk item ini juga telah dihapus. Ini akan tetap dihapus sampai induknya juga dipulihkan.', + 'recycle_bin_restore_parent' => 'Pulihkan Induk', 'recycle_bin_destroy_notification' => 'Total :count item dari tempat sampah.', 'recycle_bin_restore_notification' => 'Total :count item yang dipulihkan dari tempat sampah.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detail Peran', 'role_name' => 'Nama peran', 'role_desc' => 'Deskripsi Singkat Peran', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Otentikasi Eksternal IDs', 'role_system' => 'Izin Sistem', 'role_manage_users' => 'Kelola pengguna', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Kelola template halaman', 'role_access_api' => 'Akses Sistem API', 'role_manage_settings' => 'Kelola setelan aplikasi', + 'role_export_content' => 'Export content', 'role_asset' => 'Izin Aset', 'roles_system_warning' => 'Ketahuilah bahwa akses ke salah satu dari tiga izin di atas dapat memungkinkan pengguna untuk mengubah hak mereka sendiri atau orang lain dalam sistem. Hanya tetapkan peran dengan izin ini untuk pengguna tepercaya.', 'role_asset_desc' => 'Izin ini mengontrol akses default ke aset dalam sistem. Izin pada Buku, Bab, dan Halaman akan menggantikan izin ini.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Buat Token', 'users_api_tokens_expires' => 'Kedaluwarsa', 'users_api_tokens_docs' => 'Dokumentasi API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Buat Token API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/id/validation.php b/resources/lang/id/validation.php index 2bd5cf733..992f40329 100644 --- a/resources/lang/id/validation.php +++ b/resources/lang/id/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, tanda hubung, dan garis bawah.', 'alpha_num' => ':attribute hanya boleh berisi huruf dan angka.', 'array' => ':attribute harus berupa larik.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute harus tanggal sebelum :date.', 'between' => [ 'numeric' => ':attribute harus di antara :min dan :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute harus berupa string.', 'timezone' => ':attribute harus menjadi zona yang valid.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute sudah diambil.', 'url' => ':attribute format tidak valid.', 'uploaded' => 'Berkas tidak dapat diunggah. Server mungkin tidak menerima berkas dengan ukuran ini.', diff --git a/resources/lang/it/activities.php b/resources/lang/it/activities.php index 11c52b696..dce959828 100755 --- a/resources/lang/it/activities.php +++ b/resources/lang/it/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" è stato aggiunto ai tuoi preferiti', 'favourite_remove_notification' => '":name" è stato rimosso dai tuoi preferiti', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'ha commentato in', 'permissions_update' => 'autorizzazioni aggiornate', diff --git a/resources/lang/it/auth.php b/resources/lang/it/auth.php index a1c4b7048..3e384a797 100755 --- a/resources/lang/it/auth.php +++ b/resources/lang/it/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Benvenuto in :appName!', 'user_invite_page_text' => 'Per completare il tuo account e ottenere l\'accesso devi impostare una password che verrà utilizzata per accedere a :appName in futuro.', 'user_invite_page_confirm_button' => 'Conferma Password', - 'user_invite_success' => 'Password impostata, ora hai accesso a :appName!' + 'user_invite_success' => 'Password impostata, ora hai accesso a :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/it/common.php b/resources/lang/it/common.php index f33aa4cd3..76ee14bc4 100755 --- a/resources/lang/it/common.php +++ b/resources/lang/it/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Azzera', 'remove' => 'Rimuovi', 'add' => 'Aggiungi', + 'configure' => 'Configure', 'fullscreen' => 'Schermo intero', 'favourite' => 'Aggiungi ai Preferiti', 'unfavourite' => 'Rimuovi dai preferiti', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nessuna attività da mostrare', 'no_items' => 'Nessun elemento disponibile', 'back_to_top' => 'Torna in alto', + 'skip_to_main_content' => 'Passa al contenuto principale', 'toggle_details' => 'Mostra Dettagli', 'toggle_thumbnails' => 'Mostra Miniature', 'details' => 'Dettagli', diff --git a/resources/lang/it/entities.php b/resources/lang/it/entities.php index 9d5e8c2ab..8d3fff325 100755 --- a/resources/lang/it/entities.php +++ b/resources/lang/it/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'File Contenuto Web', 'export_pdf' => 'File PDF', 'export_text' => 'File di testo', + 'export_md' => 'File Markdown', // Permissions and restrictions 'permissions' => 'Permessi', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permessi Libreria', 'shelves_permissions_updated' => 'Permessi Libreria Aggiornati', 'shelves_permissions_active' => 'Permessi Attivi Libreria', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copia Permessi ai Libri', 'shelves_copy_permissions' => 'Copia Permessi', 'shelves_copy_permissions_explain' => 'Verranno applicati tutti i permessi della libreria ai libri contenuti. Prima di attivarlo, assicurati che ogni permesso di questa libreria sia salvato.', diff --git a/resources/lang/it/settings.php b/resources/lang/it/settings.php index 6aaca5a23..c652f10ab 100755 --- a/resources/lang/it/settings.php +++ b/resources/lang/it/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Cestino', 'recycle_bin_desc' => 'Qui è possibile ripristinare gli elementi che sono stati eliminati o scegliere di rimuoverli definitivamente dal sistema. Questo elenco non è filtrato a differenza di elenchi di attività simili nel sistema in cui vengono applicati i filtri autorizzazioni.', 'recycle_bin_deleted_item' => 'Elimina Elemento', + 'recycle_bin_deleted_parent' => 'Superiore', 'recycle_bin_deleted_by' => 'Cancellato da', 'recycle_bin_deleted_at' => 'Orario Cancellazione', 'recycle_bin_permanently_delete' => 'Elimina Definitivamente', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Elementi da Ripristinare', 'recycle_bin_restore_confirm' => 'Questa azione ripristinerà l\'elemento eliminato, compresi gli elementi figli, nella loro posizione originale. Se la posizione originale è stata eliminata, ed è ora nel cestino, anche l\'elemento padre dovrà essere ripristinato.', 'recycle_bin_restore_deleted_parent' => 'L\'elemento padre di questo elemento è stato eliminato. Questo elemento rimarrà eliminato fino a che l\'elemento padre non sarà ripristinato.', + 'recycle_bin_restore_parent' => 'Ripristina Superiore', 'recycle_bin_destroy_notification' => 'Eliminati :count elementi dal cestino.', 'recycle_bin_restore_notification' => 'Ripristinati :count elementi dal cestino.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Dettagli Ruolo', 'role_name' => 'Nome Ruolo', 'role_desc' => 'Breve Descrizione del Ruolo', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'ID Autenticazione Esterna', 'role_system' => 'Permessi di Sistema', 'role_manage_users' => 'Gestire gli utenti', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Gestisci template pagine', 'role_access_api' => 'API sistema d\'accesso', 'role_manage_settings' => 'Gestire impostazioni app', + 'role_export_content' => 'Export content', 'role_asset' => 'Permessi Entità', 'roles_system_warning' => 'Siate consapevoli che l\'accesso a uno dei tre permessi qui sopra, può consentire a un utente di modificare i propri privilegi o i privilegi di altri nel sistema. Assegna ruoli con questi permessi solo ad utenti fidati.', 'role_asset_desc' => 'Questi permessi controllano l\'accesso di default alle entità. I permessi nei Libri, Capitoli e Pagine sovrascriveranno questi.', @@ -173,7 +177,7 @@ return [ 'users_send_invite_text' => 'Puoi scegliere di inviare a questo utente un\'email di invito che permette loro di impostare la propria password altrimenti puoi impostare la password tu stesso.', 'users_send_invite_option' => 'Invia email di invito', 'users_external_auth_id' => 'ID Autenticazioni Esterna', - 'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.', + 'users_external_auth_id_desc' => 'Questo è l\'ID usato per abbinare questo utente quando si comunica con il sistema di autenticazione esterno.', 'users_password_warning' => 'Riempi solo se desideri cambiare la tua password:', 'users_system_public' => 'Questo utente rappresente qualsiasi ospite che visita il sito. Non può essere usato per effettuare il login ma è assegnato automaticamente.', 'users_delete' => 'Elimina Utente', @@ -198,25 +202,29 @@ return [ 'users_social_connected' => 'L\'account :socialAccount è stato connesso correttamente al tuo profilo.', 'users_social_disconnected' => 'L\'account :socialAccount è stato disconnesso correttamente dal tuo profilo.', 'users_api_tokens' => 'Token API', - 'users_api_tokens_none' => 'No API tokens have been created for this user', + 'users_api_tokens_none' => 'Nessun token API è stato creato per questo utente', 'users_api_tokens_create' => 'Crea Token', 'users_api_tokens_expires' => 'Scade', 'users_api_tokens_docs' => 'Documentazione API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Crea Token API', 'user_api_token_name' => 'Nome', - 'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.', + 'user_api_token_name_desc' => 'Assegna al tuo token un nome leggibile per ricordarne la funzionalità in futuro.', 'user_api_token_expiry' => 'Data di scadenza', - 'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.', - 'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.', + 'user_api_token_expiry_desc' => 'Imposta una data di scadenza per questo token. Dopo questa data, le richieste che utilizzeranno questo token non funzioneranno più. Lasciando questo campo vuoto si imposterà la scadenza tra 100 anni.', + 'user_api_token_create_secret_message' => 'Immediatamente dopo aver creato questo token, un "Token ID" e un "Segreto Token" saranno generati e mostrati. Il segreto verrà mostrato unicamente questa volta, assicurati, quindi, di copiare il valore in un posto sicuro prima di procedere.', 'user_api_token_create_success' => 'Token API creato correttamente', 'user_api_token_update_success' => 'Token API aggiornato correttamente', 'user_api_token' => 'Token API', 'user_api_token_id' => 'Token ID', - 'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.', + 'user_api_token_id_desc' => 'Questo è un identificativo non modificabile generato dal sistema per questo token e che sarà necessario fornire per le richieste tramite API.', 'user_api_token_secret' => 'Token Segreto', - 'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.', + 'user_api_token_secret_desc' => 'Questo è un segreto generato dal sistema per questo token che sarà necessario fornire per le richieste via API. Questo valore sarà visibile unicamente in questo momento pertanto copialo in un posto sicuro.', 'user_api_token_created' => 'Token Aggiornato :timeAgo', 'user_api_token_updated' => 'Token Aggiornato :timeAgo', 'user_api_token_delete' => 'Elimina Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/it/validation.php b/resources/lang/it/validation.php index 69d023688..efd11be10 100755 --- a/resources/lang/it/validation.php +++ b/resources/lang/it/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute deve contenere solo lettere, numeri e meno.', 'alpha_num' => ':attribute deve contenere solo lettere e numeri.', 'array' => ':attribute deve essere un array.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute deve essere una data prima del :date.', 'between' => [ 'numeric' => 'Il campo :attribute deve essere tra :min e :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute deve essere una stringa.', 'timezone' => ':attribute deve essere una zona valida.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute è già preso.', 'url' => 'Il formato :attribute non è valido.', 'uploaded' => 'Il file non può essere caricato. Il server potrebbe non accettare file di questa dimensione.', diff --git a/resources/lang/ja/activities.php b/resources/lang/ja/activities.php index fd119a304..3dc749b67 100644 --- a/resources/lang/ja/activities.php +++ b/resources/lang/ja/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'コメントする', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/ja/auth.php b/resources/lang/ja/auth.php index 6163a5fc1..8dcac7aa4 100644 --- a/resources/lang/ja/auth.php +++ b/resources/lang/ja/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Welcome to :appName!', 'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.', 'user_invite_page_confirm_button' => 'Confirm Password', - 'user_invite_success' => 'Password set, you now have access to :appName!' + 'user_invite_success' => 'Password set, you now have access to :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/ja/common.php b/resources/lang/ja/common.php index e52da90a1..a5f2f9429 100644 --- a/resources/lang/ja/common.php +++ b/resources/lang/ja/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'リセット', 'remove' => '削除', 'add' => '追加', + 'configure' => 'Configure', 'fullscreen' => 'Fullscreen', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => '表示するアクティビティがありません', 'no_items' => 'アイテムはありません', 'back_to_top' => '上に戻る', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => '概要の表示切替', 'toggle_thumbnails' => 'Toggle Thumbnails', 'details' => '詳細', diff --git a/resources/lang/ja/entities.php b/resources/lang/ja/entities.php index 6515305e4..1c9dafc43 100644 --- a/resources/lang/ja/entities.php +++ b/resources/lang/ja/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Webページ', 'export_pdf' => 'PDF', 'export_text' => 'テキストファイル', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => '権限', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Bookshelf Permissions', 'shelves_permissions_updated' => 'Bookshelf Permissions Updated', 'shelves_permissions_active' => 'Bookshelf Permissions Active', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books', 'shelves_copy_permissions' => 'Copy Permissions', 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.', diff --git a/resources/lang/ja/settings.php b/resources/lang/ja/settings.php index b972c9b61..17532de41 100644 --- a/resources/lang/ja/settings.php +++ b/resources/lang/ja/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => '概要', 'role_name' => '役割名', 'role_desc' => '役割の説明', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'システム権限', 'role_manage_users' => 'ユーザ管理', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Manage page templates', 'role_access_api' => 'Access system API', 'role_manage_settings' => 'アプリケーション設定の管理', + 'role_export_content' => 'Export content', 'role_asset' => 'アセット権限', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => '各アセットに対するデフォルトの権限を設定します。ここで設定した権限が優先されます。', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Create Token', 'users_api_tokens_expires' => 'Expires', 'users_api_tokens_docs' => 'API Documentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/ja/validation.php b/resources/lang/ja/validation.php index 7d9987ce0..2ef8b0119 100644 --- a/resources/lang/ja/validation.php +++ b/resources/lang/ja/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attributeは文字, 数値, ハイフンのみが含められます。', 'alpha_num' => ':attributeは文字と数値のみが含められます。', 'array' => ':attributeは配列である必要があります。', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attributeは:date以前である必要があります。', 'between' => [ 'numeric' => ':attributeは:min〜:maxである必要があります。', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attributeは文字列である必要があります。', 'timezone' => ':attributeは正しいタイムゾーンである必要があります。', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attributeは既に使用されています。', 'url' => ':attributeのフォーマットは不正です。', 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', diff --git a/resources/lang/ko/activities.php b/resources/lang/ko/activities.php index ed0b66112..ee694f073 100644 --- a/resources/lang/ko/activities.php +++ b/resources/lang/ko/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => '댓글 쓰기', 'permissions_update' => 'updated permissions', diff --git a/resources/lang/ko/auth.php b/resources/lang/ko/auth.php index 0bff8724c..ce65f3ecc 100644 --- a/resources/lang/ko/auth.php +++ b/resources/lang/ko/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => ':appName에 오신 것을 환영합니다!', 'user_invite_page_text' => ':appName에 로그인할 때 입력할 비밀번호를 설정하세요.', 'user_invite_page_confirm_button' => '비밀번호 확인', - 'user_invite_success' => '암호가 설정되었고, 이제 :appName에 접근할 수 있습니다.' + 'user_invite_success' => '암호가 설정되었고, 이제 :appName에 접근할 수 있습니다.', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/ko/common.php b/resources/lang/ko/common.php index 3cc46ab49..2349ab735 100644 --- a/resources/lang/ko/common.php +++ b/resources/lang/ko/common.php @@ -39,6 +39,7 @@ return [ 'reset' => '리셋', 'remove' => '제거', 'add' => '추가', + 'configure' => 'Configure', 'fullscreen' => '전체화면', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => '활동 없음', 'no_items' => '항목 없음', 'back_to_top' => '맨 위로', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => '내용 보기', 'toggle_thumbnails' => '섬네일 보기', 'details' => '정보', diff --git a/resources/lang/ko/entities.php b/resources/lang/ko/entities.php index e286fee8c..aa25aa646 100644 --- a/resources/lang/ko/entities.php +++ b/resources/lang/ko/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Contained Web(.html) 파일', 'export_pdf' => 'PDF 파일', 'export_text' => 'Plain Text(.txt) 파일', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => '권한', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => '서가 권한', 'shelves_permissions_updated' => '서가 권한 바꿈', 'shelves_permissions_active' => '서가 권한 허용함', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => '권한 맞춤', 'shelves_copy_permissions' => '실행', 'shelves_copy_permissions_explain' => '서가의 모든 책자에 이 권한을 적용합니다. 서가의 권한을 저장했는지 확인하세요.', diff --git a/resources/lang/ko/settings.php b/resources/lang/ko/settings.php index 95a681eca..46856cfe4 100755 --- a/resources/lang/ko/settings.php +++ b/resources/lang/ko/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => '권한 정보', 'role_name' => '권한 이름', 'role_desc' => '설명', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'LDAP 확인', 'role_system' => '시스템 권한', 'role_manage_users' => '사용자 관리', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => '템플릿 관리', 'role_access_api' => '시스템 접근 API', 'role_manage_settings' => '사이트 설정 관리', + 'role_export_content' => 'Export content', 'role_asset' => '권한 항목', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => '책자, 챕터, 문서별 권한은 이 설정에 우선합니다.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => '토큰 만들기', 'users_api_tokens_expires' => '만료', 'users_api_tokens_docs' => 'API 설명서', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'API 토큰 만들기', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/ko/validation.php b/resources/lang/ko/validation.php index 6754d9620..ef8328103 100644 --- a/resources/lang/ko/validation.php +++ b/resources/lang/ko/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute(을)를 문자, 숫자, -, _로만 구성하세요.', 'alpha_num' => ':attribute(을)를 문자, 숫자로만 구성하세요.', 'array' => ':attribute(을)를 배열로 구성하세요.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute(을)를 :date 전으로 설정하세요.', 'between' => [ 'numeric' => ':attribute(을)를 :min~:max(으)로 구성하세요.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute(을)를 문자로 구성하세요.', 'timezone' => ':attribute(을)를 유효한 시간대로 구성하세요.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute(은)는 이미 있습니다.', 'url' => ':attribute(은)는 유효하지 않은 형식입니다.', 'uploaded' => '파일 크기가 서버에서 허용하는 수치를 넘습니다.', diff --git a/resources/lang/lt/activities.php b/resources/lang/lt/activities.php new file mode 100644 index 000000000..b1289342d --- /dev/null +++ b/resources/lang/lt/activities.php @@ -0,0 +1,57 @@ + 'sukurtas puslapis', + 'page_create_notification' => 'Puslapis sukurtas sėkmingai', + 'page_update' => 'atnaujintas puslapis', + 'page_update_notification' => 'Puslapis sėkmingai atnaujintas', + 'page_delete' => 'ištrintas puslapis', + 'page_delete_notification' => 'Puslapis sėkmingai ištrintas', + 'page_restore' => 'atkurtas puslapis', + 'page_restore_notification' => 'Puslapis sėkmingai atkurtas', + 'page_move' => 'perkeltas puslapis', + + // Chapters + 'chapter_create' => 'sukurtas skyrius', + 'chapter_create_notification' => 'Skyrius sėkmingai sukurtas', + 'chapter_update' => 'atnaujintas skyrius', + 'chapter_update_notification' => 'Skyrius sekmingai atnaujintas', + 'chapter_delete' => 'ištrintas skyrius', + 'chapter_delete_notification' => 'Skyrius sėkmingai ištrintas', + 'chapter_move' => 'perkeltas skyrius', + + // Books + 'book_create' => 'sukurta knyga', + 'book_create_notification' => 'Knyga sėkmingai sukurta', + 'book_update' => 'atnaujinta knyga', + 'book_update_notification' => 'Knyga sėkmingai atnaujinta', + 'book_delete' => 'ištrinta knyga', + 'book_delete_notification' => 'Knyga sėkmingai ištrinta', + 'book_sort' => 'surūšiuota knyga', + 'book_sort_notification' => 'Knyga sėkmingai perrūšiuota', + + // Bookshelves + 'bookshelf_create' => 'sukurta knygų lentyna', + 'bookshelf_create_notification' => 'Knygų lentyna sėkmingai sukurta', + 'bookshelf_update' => 'atnaujinta knygų lentyna', + 'bookshelf_update_notification' => 'Knygų lentyna sėkmingai atnaujinta', + 'bookshelf_delete' => 'ištrinta knygų lentyna', + 'bookshelf_delete_notification' => 'Knygų lentyna sėkmingai ištrinta', + + // Favourites + 'favourite_add_notification' => '":name" has been added to your favourites', + 'favourite_remove_notification' => '":name" has been removed from your favourites', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + + // Other + 'commented_on' => 'pakomentavo', + 'permissions_update' => 'atnaujinti leidimai', +]; diff --git a/resources/lang/lt/auth.php b/resources/lang/lt/auth.php new file mode 100644 index 000000000..f9bf271f6 --- /dev/null +++ b/resources/lang/lt/auth.php @@ -0,0 +1,112 @@ + 'Šie įgaliojimai neatitinka mūsų įrašų.', + 'throttle' => 'Per daug prisijungimo bandymų. Prašome pabandyti dar kartą po :seconds sekundžių.', + + // Login & Register + 'sign_up' => 'Užsiregistruoti', + 'log_in' => 'Prisijungti', + 'log_in_with' => 'Prisijungti su :socialDriver', + 'sign_up_with' => 'Užsiregistruoti su :socialDriver', + 'logout' => 'Atsijungti', + + 'name' => 'Pavadinimas', + 'username' => 'Vartotojo vardas', + 'email' => 'Elektroninis paštas', + 'password' => 'Slaptažodis', + 'password_confirm' => 'Patvirtinti slaptažodį', + 'password_hint' => 'Privalo būti daugiau nei 7 simboliai', + 'forgot_password' => 'Pamiršote slaptažodį?', + 'remember_me' => 'Prisimink mane', + 'ldap_email_hint' => 'Prašome įvesti elektroninį paštą, kad galėtume naudotis šia paskyra.', + 'create_account' => 'Sukurti paskyrą', + 'already_have_account' => 'Jau turite paskyrą?', + 'dont_have_account' => 'Neturite paskyros?', + 'social_login' => 'Socialinis prisijungimas', + 'social_registration' => 'Socialinė registracija', + 'social_registration_text' => 'Užsiregistruoti ir prisijungti naudojantis kita paslauga.', + + 'register_thanks' => 'Ačiū, kad užsiregistravote!', + 'register_confirm' => 'Prašome patikrinti savo elektroninį paštą ir paspausti patvirtinimo mygtuką, kad gautumėte leidimą į :appName.', + 'registrations_disabled' => 'Registracijos šiuo metu negalimos', + 'registration_email_domain_invalid' => 'Elektroninio pašto domenas neturi prieigos prie šios programos', + 'register_success' => 'Ačiū už prisijungimą! Dabar jūs užsiregistravote ir prisijungėte.', + + + // Password Reset + 'reset_password' => 'Pakeisti slaptažodį', + 'reset_password_send_instructions' => 'Įveskite savo elektroninį paštą žemiau ir jums bus išsiųstas elektroninis laiškas su slaptažodžio nustatymo nuoroda.', + 'reset_password_send_button' => 'Atsiųsti atsatymo nuorodą', + 'reset_password_sent' => 'Slaptažodžio nustatymo nuoroda bus išsiųsta :email jeigu elektroninio pašto adresas bus rastas sistemoje.', + 'reset_password_success' => 'Jūsų slaptažodis buvo sėkmingai atnaujintas.', + 'email_reset_subject' => 'Atnaujinti jūsų :appName slaptažodį', + 'email_reset_text' => 'Šį laišką gaunate, nes mes gavome slaptažodžio atnaujinimo užklausą iš jūsų paskyros.', + 'email_reset_not_requested' => 'Jeigu jums nereikia slaptažodžio atnaujinimo, tolimesnių veiksmų atlikti nereikia.', + + + // Email Confirmation + 'email_confirm_subject' => 'Patvirtinkite savo elektroninį paštą :appName', + 'email_confirm_greeting' => 'Ačiū už prisijungimą prie :appName!', + 'email_confirm_text' => 'Prašome patvirtinti savo elektroninio pašto adresą paspaudus mygtuką žemiau:', + 'email_confirm_action' => 'Patvirtinkite elektroninį paštą', + 'email_confirm_send_error' => 'Būtinas elektroninio laiško patviritnimas, bet sistema negali išsiųsti laiško. Susisiekite su administratoriumi, kad užtikrintumėte, jog elektroninis paštas atsinaujino teisingai.', + 'email_confirm_success' => 'Jūsų elektroninis paštas buvo patvirtintas!', + 'email_confirm_resent' => 'Elektroninio pašto patvirtinimas persiųstas, prašome patikrinti pašto dėžutę.', + + 'email_not_confirmed' => 'Elektroninis paštas nepatvirtintas', + 'email_not_confirmed_text' => 'Jūsų elektroninis paštas dar vis nepatvirtintas.', + 'email_not_confirmed_click_link' => 'Prašome paspausti nuorodą elektroniniame pašte, kuri buvo išsiųsta iš karto po registracijos.', + 'email_not_confirmed_resend' => 'Jeigu nerandate elektroninio laiško, galite dar kartą išsiųsti patvirtinimo elektroninį laišką, pateikdami žemiau esančią formą.', + 'email_not_confirmed_resend_button' => 'Persiųsti patvirtinimo laišką', + + // User Invite + 'user_invite_email_subject' => 'Jūs buvote pakviestas prisijungti prie :appName!', + 'user_invite_email_greeting' => 'Paskyra buvo sukurta jums :appName.', + 'user_invite_email_text' => 'Paspauskite mygtuką žemiau, kad sukurtumėte paskyros slaptažodį ir gautumėte prieigą:', + 'user_invite_email_action' => 'Sukurti paskyros slaptažodį', + 'user_invite_page_welcome' => 'Sveiki atvykę į :appName!', + 'user_invite_page_text' => 'Norėdami galutinai pabaigti paskyrą ir gauti prieigą jums reikia nustatyti slaptažodį, kuris bus naudojamas prisijungiant prie :appName ateities vizitų metu.', + 'user_invite_page_confirm_button' => 'Patvirtinti slaptažodį', + 'user_invite_success' => 'Slaptažodis nustatytas, dabar turite prieigą prie :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', +]; \ No newline at end of file diff --git a/resources/lang/lt/common.php b/resources/lang/lt/common.php new file mode 100644 index 000000000..7ed434b98 --- /dev/null +++ b/resources/lang/lt/common.php @@ -0,0 +1,95 @@ + 'Atšaukti', + 'confirm' => 'Patvirtinti', + 'back' => 'Grįžti', + 'save' => 'Išsaugoti', + 'continue' => 'Praleisti', + 'select' => 'Pasirinkti', + 'toggle_all' => 'Perjungti visus', + 'more' => 'Daugiau', + + // Form Labels + 'name' => 'Pavadinimas', + 'description' => 'Apibūdinimas', + 'role' => 'Vaidmuo', + 'cover_image' => 'Viršelio nuotrauka', + 'cover_image_description' => 'Ši nuotrauka turi būti maždaug 440x250px.', + + // Actions + 'actions' => 'Veiksmai', + 'view' => 'Rodyti', + 'view_all' => 'Rodyti visus', + 'create' => 'Sukurti', + 'update' => 'Atnaujinti', + 'edit' => 'Redaguoti', + 'sort' => 'Rūšiuoti', + 'move' => 'Perkelti', + 'copy' => 'Kopijuoti', + 'reply' => 'Atsakyti', + 'delete' => 'Ištrinti', + 'delete_confirm' => 'Patvirtinti ištrynimą', + 'search' => 'Paieška', + 'search_clear' => 'Išvalyti paiešką', + 'reset' => 'Atsatyti', + 'remove' => 'Pašalinti', + 'add' => 'Pridėti', + 'configure' => 'Configure', + 'fullscreen' => 'Visas ekranas', + 'favourite' => 'Favourite', + 'unfavourite' => 'Unfavourite', + 'next' => 'Next', + 'previous' => 'Previous', + + // Sort Options + 'sort_options' => 'Rūšiuoti pasirinkimus', + 'sort_direction_toggle' => 'Rūšiuoti krypties perjungimus', + 'sort_ascending' => 'Rūšiuoti didėjančia tvarka', + 'sort_descending' => 'Rūšiuoti mažėjančia tvarka', + 'sort_name' => 'Pavadinimas', + 'sort_default' => 'Numatytas', + 'sort_created_at' => 'Sukurta data', + 'sort_updated_at' => 'Atnaujinta data', + + // Misc + 'deleted_user' => 'Ištrinti naudotoją', + 'no_activity' => 'Nėra veiklų', + 'no_items' => 'Nėra elementų', + 'back_to_top' => 'Grįžti į pradžią', + 'skip_to_main_content' => 'Skip to main content', + 'toggle_details' => 'Perjungti detales', + 'toggle_thumbnails' => 'Perjungti miniatūras', + 'details' => 'Detalės', + 'grid_view' => 'Tinklelio vaizdas', + 'list_view' => 'Sąrašas', + 'default' => 'Numatytas', + 'breadcrumb' => 'Duonos rėžis', + + // Header + 'header_menu_expand' => 'Plėsti antraštės meniu', + 'profile_menu' => 'Profilio meniu', + 'view_profile' => 'Rodyti porofilį', + 'edit_profile' => 'Redaguoti profilį', + 'dark_mode' => 'Tamsus rėžimas', + 'light_mode' => 'Šviesus rėžimas', + + // Layout tabs + 'tab_info' => 'Informacija', + 'tab_info_label' => 'Skirtukas: Rodyti antrinę informaciją', + 'tab_content' => 'Turinys', + 'tab_content_label' => 'Skirtukas: Rodyti pirminę informaciją', + + // Email Content + 'email_action_help' => 'Jeigu kyla problemų spaudžiant :actionText: mygtuką, nukopijuokite ir įklijuokite URL į savo naršyklę.', + 'email_rights' => 'Visos teisės rezervuotos', + + // Footer Link Options + // Not directly used but available for convenience to users. + 'privacy_policy' => 'Privatumo politika', + 'terms_of_service' => 'Paslaugų teikimo paslaugos', +]; diff --git a/resources/lang/lt/components.php b/resources/lang/lt/components.php new file mode 100644 index 000000000..ce573dac8 --- /dev/null +++ b/resources/lang/lt/components.php @@ -0,0 +1,34 @@ + 'Nuotraukų pasirinkimas', + 'image_all' => 'Visi', + 'image_all_title' => 'Rodyti visas nuotraukas', + 'image_book_title' => 'Peržiūrėti nuotraukas, įkeltas į šią knygą', + 'image_page_title' => 'Peržiūrėti nuotraukas, įkeltas į šį puslapį', + 'image_search_hint' => 'Ieškoti pagal nuotraukos pavadinimą', + 'image_uploaded' => 'Įkelta :uploadedDate', + 'image_load_more' => 'Rodyti daugiau', + 'image_image_name' => 'Nuotraukos pavadinimas', + 'image_delete_used' => 'Ši nuotrauka yra naudojama puslapyje žemiau.', + 'image_delete_confirm_text' => 'Ar jūs esate tikri, kad norite ištrinti šią nuotrauką?', + 'image_select_image' => 'Pasirinkti nuotrauką', + 'image_dropzone' => 'Tempkite nuotraukas arba spauskite šia, kad įkeltumėte', + 'images_deleted' => 'Nuotraukos ištrintos', + 'image_preview' => 'Nuotraukų peržiūra', + 'image_upload_success' => 'Nuotrauka įkelta sėkmingai', + 'image_update_success' => 'Nuotraukos detalės sėkmingai atnaujintos', + 'image_delete_success' => 'Nuotrauka sėkmingai ištrinti', + 'image_upload_remove' => 'Pašalinti', + + // Code Editor + 'code_editor' => 'Redaguoti kodą', + 'code_language' => 'Kodo kalba', + 'code_content' => 'Kodo turinys', + 'code_session_history' => 'Sesijos istorija', + 'code_save' => 'Išsaugoti kodą', +]; diff --git a/resources/lang/lt/entities.php b/resources/lang/lt/entities.php new file mode 100644 index 000000000..2b9a5e29b --- /dev/null +++ b/resources/lang/lt/entities.php @@ -0,0 +1,324 @@ + 'Neseniai sukurtas', + 'recently_created_pages' => 'Neseniai sukurti puslapiai', + 'recently_updated_pages' => 'Neseniai atnaujinti puslapiai', + 'recently_created_chapters' => 'Neseniai sukurti skyriai', + 'recently_created_books' => 'Neseniai sukurtos knygos', + 'recently_created_shelves' => 'Neseniai sukurtos lentynos', + 'recently_update' => 'Neseniai atnaujinta', + 'recently_viewed' => 'Neseniai peržiūrėta', + 'recent_activity' => 'Paskutiniai veiksmai', + 'create_now' => 'Sukurti vieną dabar', + 'revisions' => 'Pataisymai', + 'meta_revision' => 'Pataisymas #:revisionCount', + 'meta_created' => 'Sukurta :timeLength', + 'meta_created_name' => 'Sukurta :timeLength naudotojo :user', + 'meta_updated' => 'Atnaujintas :timeLength', + 'meta_updated_name' => 'Atnaujinta :timeLength naudotojo :user', + 'meta_owned_name' => 'Priklauso :user', + 'entity_select' => 'Pasirinkti subjektą', + 'images' => 'Nuotraukos', + 'my_recent_drafts' => 'Naujausi išsaugoti juodraščiai', + 'my_recently_viewed' => 'Neseniai peržiūrėti', + 'my_most_viewed_favourites' => 'My Most Viewed Favourites', + 'my_favourites' => 'My Favourites', + 'no_pages_viewed' => 'Jūs neperžiūrėjote nei vieno puslapio', + 'no_pages_recently_created' => 'Nebuvos sukurta jokių puslapių', + 'no_pages_recently_updated' => 'Nebuvo atnaujinta jokių puslapių', + 'export' => 'Eksportuoti', + 'export_html' => 'Sudėtinis žiniatinklio failas', + 'export_pdf' => 'PDF failas', + 'export_text' => 'Paprastas failo tekstas', + 'export_md' => 'Markdown File', + + // Permissions and restrictions + 'permissions' => 'Leidimai', + 'permissions_intro' => 'Įgalinus šias teises, pirmenybė bus teikiama visiems nustatytiems vaidmenų leidimams.', + 'permissions_enable' => 'Įgalinti pasirinktus leidimus', + 'permissions_save' => 'Išsaugoti leidimus', + 'permissions_owner' => 'Savininkas', + + // Search + 'search_results' => 'Ieškoti rezultatų', + 'search_total_results_found' => ':count rastas rezultatas|:count iš viso rezultatų rasta', + 'search_clear' => 'Išvalyti paiešką', + 'search_no_pages' => 'Nėra puslapių pagal šią paiešką', + 'search_for_term' => 'Ieškoti pagal :term', + 'search_more' => 'Daugiau rezultatų', + 'search_advanced' => 'Išplėstinė paieška', + 'search_terms' => 'Ieškoti terminų', + 'search_content_type' => 'Turinio tipas', + 'search_exact_matches' => 'Tikslūs atitikmenys', + 'search_tags' => 'Žymių paieškos', + 'search_options' => 'Parinktys', + 'search_viewed_by_me' => 'Mano peržiūrėta', + 'search_not_viewed_by_me' => 'Mano neperžiūrėta', + 'search_permissions_set' => 'Nustatyti leidimus', + 'search_created_by_me' => 'Mano sukurta', + 'search_updated_by_me' => 'Mano atnaujinimas', + 'search_owned_by_me' => 'Priklauso man', + 'search_date_options' => 'Datos parinktys', + 'search_updated_before' => 'Atnaujinta prieš', + 'search_updated_after' => 'Atnaujinta po', + 'search_created_before' => 'Sukurta prieš', + 'search_created_after' => 'Sukurta po', + 'search_set_date' => 'Nustatyti datą', + 'search_update' => 'Atnaujinti paiešką', + + // Shelves + 'shelf' => 'Lentyna', + 'shelves' => 'Lentynos', + 'x_shelves' => ':count lentyna|:count lentynos', + 'shelves_long' => 'Knygų lentynos', + 'shelves_empty' => 'Nebuvo sukurtos jokios lentynos', + 'shelves_create' => 'Sukurti naują lentyną', + 'shelves_popular' => 'Populiarios lentynos', + 'shelves_new' => 'Naujos lentynos', + 'shelves_new_action' => 'Nauja lentyna', + 'shelves_popular_empty' => 'Populiariausios knygos pasirodys čia.', + 'shelves_new_empty' => 'Visai neseniai sukurtos lentynos pasirodys čia.', + 'shelves_save' => 'Išsaugoti lenyną', + 'shelves_books' => 'Knygos šioje lentynoje', + 'shelves_add_books' => 'Pridėti knygas į šią lentyną', + 'shelves_drag_books' => 'Vilkite knygas čia, kad pridėtumėte jas į šią lentyną', + 'shelves_empty_contents' => 'Ši lentyną neturi jokių pridėtų knygų', + 'shelves_edit_and_assign' => 'Redaguoti lentyną, kad pridėti knygų', + 'shelves_edit_named' => 'Redaguoti knygų lentyną :name', + 'shelves_edit' => 'Redaguoti knygų lentyną', + 'shelves_delete' => 'Ištrinti knygų lentyną', + 'shelves_delete_named' => 'Ištrinti knygų lentyną :name', + 'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.", + 'shelves_delete_confirmation' => 'Ar jūs esate tikri, kad norite ištrinti šią knygų lentyną?', + 'shelves_permissions' => 'Knygų lentynos leidimai', + 'shelves_permissions_updated' => 'Knygų lentynos leidimai atnaujinti', + 'shelves_permissions_active' => 'Knygų lentynos leidimai aktyvūs', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', + 'shelves_copy_permissions_to_books' => 'Kopijuoti leidimus knygoms', + 'shelves_copy_permissions' => 'Kopijuoti leidimus', + 'shelves_copy_permissions_explain' => 'Visoms knygoms, esančioms šioje knygų lentynoje, bus taikomi dabartiniai leidimų nustatymai. Prieš suaktyvindami įsitikinkite, kad visi šios knygų lentynos leidimų pakeitimai buvo išsaugoti.', + 'shelves_copy_permission_success' => 'Knygų lentynos leidimai nukopijuoti į :count knygas', + + // Books + 'book' => 'Knyga', + 'books' => 'Knygos', + 'x_books' => ':count knyga|:count knygos', + 'books_empty' => 'Nebuvo sukurta jokių knygų', + 'books_popular' => 'Populiarios knygos', + 'books_recent' => 'Naujos knygos', + 'books_new' => 'Naujos knygos', + 'books_new_action' => 'Nauja knyga', + 'books_popular_empty' => 'Čia pasirodys pačios populiariausios knygos.', + 'books_new_empty' => 'Čia pasirodys naujausios sukurtos knygos', + 'books_create' => 'Sukurti naują knygą', + 'books_delete' => 'Ištrinti knygą', + 'books_delete_named' => 'Ištrinti knygą :bookName', + 'books_delete_explain' => 'Tai ištrins knygą su pavadinimu \':bookName\'. Visi puslapiai ir skyriai bus pašalinti.', + 'books_delete_confirmation' => 'Ar jūs esate tikri, kad norite ištrinti šią knygą?', + 'books_edit' => 'Redaguoti knygą', + 'books_edit_named' => 'Redaguoti knygą :bookName', + 'books_form_book_name' => 'Knygos pavadinimas', + 'books_save' => 'Išsaugoti knygą', + 'books_permissions' => 'Knygos leidimas', + 'books_permissions_updated' => 'Knygos leidimas atnaujintas', + 'books_empty_contents' => 'Jokių puslapių ar skyrių nebuvo skurta šiai knygai', + 'books_empty_create_page' => 'Sukurti naują puslapį', + 'books_empty_sort_current_book' => 'Rūšiuoti dabartinę knygą', + 'books_empty_add_chapter' => 'Pridėti skyrių', + 'books_permissions_active' => 'Knygos leidimas aktyvus', + 'books_search_this' => 'Ieškoti šioje knygoje', + 'books_navigation' => 'Knygos naršymas', + 'books_sort' => 'Rūšiuoti pagal knygos turinį', + 'books_sort_named' => 'Rūšiuoti knygą :bookName', + 'books_sort_name' => 'Rūšiuoti pagal vardą', + 'books_sort_created' => 'Rūšiuoti pagal sukūrimo datą', + 'books_sort_updated' => 'Rūšiuoti pagal atnaujinimo datą', + 'books_sort_chapters_first' => 'Skyriaus pradžia', + 'books_sort_chapters_last' => 'Skyriaus pabaiga', + 'books_sort_show_other' => 'Rodyti kitas knygas', + 'books_sort_save' => 'Išsaugoti naują įsakymą', + + // Chapters + 'chapter' => 'Skyrius', + 'chapters' => 'Skyriai', + 'x_chapters' => ':count skyrius|:count skyriai', + 'chapters_popular' => 'Populiarūs skyriai', + 'chapters_new' => 'Naujas skyrius', + 'chapters_create' => 'Sukurti naują skyrių', + 'chapters_delete' => 'Ištrinti skyrių', + 'chapters_delete_named' => 'Ištrinti skyrių :chapterName', + 'chapters_delete_explain' => 'Tai ištrins skyrių su pavadinimu \':chapterName\. Visi puslapiai, esantys šiame skyriuje, taip pat bus ištrinti.', + 'chapters_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį skyrių?', + 'chapters_edit' => 'Redaguoti skyrių', + 'chapters_edit_named' => 'Redaguoti skyrių :chapterName', + 'chapters_save' => 'Išsaugoti skyrių', + 'chapters_move' => 'Perkelti skyrių', + 'chapters_move_named' => 'Perkelti skyrių :chapterName', + 'chapter_move_success' => 'Skyrius perkeltas į :bookName', + 'chapters_permissions' => 'Skyriaus leidimai', + 'chapters_empty' => 'Šiuo metu skyriuje nėra puslapių', + 'chapters_permissions_active' => 'Skyriaus leidimai aktyvūs', + 'chapters_permissions_success' => 'Skyriaus leidimai atnaujinti', + 'chapters_search_this' => 'Ieškoti šio skyriaus', + + // Pages + 'page' => 'Puslapis', + 'pages' => 'Puslapiai', + 'x_pages' => ':count puslapis|:count puslapiai', + 'pages_popular' => 'Populiarūs puslapiai', + 'pages_new' => 'Naujas puslapis', + 'pages_attachments' => 'Priedai', + 'pages_navigation' => 'Puslapių navigacija', + 'pages_delete' => 'Ištrinti puslapį', + 'pages_delete_named' => 'Ištrinti puslapį :pageName', + 'pages_delete_draft_named' => 'Ištrinti juodraščio puslapį :pageName', + 'pages_delete_draft' => 'Ištrinti juodraščio puslapį', + 'pages_delete_success' => 'Puslapis ištrintas', + 'pages_delete_draft_success' => 'Juodraščio puslapis ištrintas', + 'pages_delete_confirm' => 'Ar esate tikri, kad norite ištrinti šį puslapį?', + 'pages_delete_draft_confirm' => 'Ar esate tikri, kad norite ištrinti šį juodraščio puslapį?', + 'pages_editing_named' => 'Redaguojamas puslapis :pageName', + 'pages_edit_draft_options' => 'Juodrasčio pasirinkimai', + 'pages_edit_save_draft' => 'Išsaugoti juodraštį', + 'pages_edit_draft' => 'Redaguoti juodraščio puslapį', + 'pages_editing_draft' => 'Redaguojamas juodraštis', + 'pages_editing_page' => 'Redaguojamas puslapis', + 'pages_edit_draft_save_at' => 'Juodraštis išsaugotas', + 'pages_edit_delete_draft' => 'Ištrinti juodraštį', + 'pages_edit_discard_draft' => 'Išmesti juodraštį', + 'pages_edit_set_changelog' => 'Nustatyti keitimo žurnalą', + 'pages_edit_enter_changelog_desc' => 'Įveskite trumpus, jūsų atliktus, pokyčių aprašymus', + 'pages_edit_enter_changelog' => 'Įeiti į keitimo žurnalą', + 'pages_save' => 'Išsaugoti puslapį', + 'pages_title' => 'Puslapio antraštė', + 'pages_name' => 'Puslapio pavadinimas', + 'pages_md_editor' => 'Redaguotojas', + 'pages_md_preview' => 'Peržiūra', + 'pages_md_insert_image' => 'Įterpti nuotrauką', + 'pages_md_insert_link' => 'Įterpti subjekto nuorodą', + 'pages_md_insert_drawing' => 'Įterpti piešinį', + 'pages_not_in_chapter' => 'Puslapio nėra skyriuje', + 'pages_move' => 'Perkelti puslapį', + 'pages_move_success' => 'Puslapis perkeltas į ":parentName"', + 'pages_copy' => 'Nukopijuoti puslapį', + 'pages_copy_desination' => 'Nukopijuoti tikslą', + 'pages_copy_success' => 'Puslapis sėkmingai nukopijuotas', + 'pages_permissions' => 'Puslapio leidimai', + 'pages_permissions_success' => 'Puslapio leidimai atnaujinti', + 'pages_revision' => 'Peržiūra', + 'pages_revisions' => 'Puslapio peržiūros', + 'pages_revisions_named' => 'Peržiūros puslapio :pageName', + 'pages_revision_named' => 'Peržiūra puslapio :pageName', + 'pages_revision_restored_from' => 'Atkurta iš #:id; :summary', + 'pages_revisions_created_by' => 'Sukurta', + 'pages_revisions_date' => 'Peržiūros data', + 'pages_revisions_number' => '#', + 'pages_revisions_numbered' => 'Peržiūra #:id', + 'pages_revisions_numbered_changes' => 'Peržiūros #:id pokyčiai', + 'pages_revisions_changelog' => 'Keitimo žurnalas', + 'pages_revisions_changes' => 'Pakeitimai', + 'pages_revisions_current' => 'Dabartinė versija', + 'pages_revisions_preview' => 'Peržiūra', + 'pages_revisions_restore' => 'Atkurti', + 'pages_revisions_none' => 'Šis puslapis neturi peržiūrų', + 'pages_copy_link' => 'Kopijuoti nuorodą', + 'pages_edit_content_link' => 'Redaguoti turinį', + 'pages_permissions_active' => 'Puslapio leidimai aktyvūs', + 'pages_initial_revision' => 'Pradinis skelbimas', + 'pages_initial_name' => 'Naujas puslapis', + 'pages_editing_draft_notification' => 'Dabar jūs redaguojate juodraštį, kuris paskutinį kartą buvo išsaugotas :timeDiff', + 'pages_draft_edited_notification' => 'Šis puslapis buvo redaguotas iki to laiko. Rekomenduojame jums išmesti šį juodraštį.', + 'pages_draft_edit_active' => [ + 'start_a' => ':count naudotojai pradėjo redaguoti šį puslapį', + 'start_b' => ':userName pradėjo redaguoti šį puslapį', + 'time_a' => 'nuo puslapio paskutinio atnaujinimo', + 'time_b' => 'paskutinėmis :minCount minutėmis', + 'message' => ':start :time. Pasistenkite neperrašyti vienas kito atnaujinimų!', + ], + 'pages_draft_discarded' => 'Juodraštis atmestas, redaguotojas atnaujintas dabartinis puslapio turinys', + 'pages_specific' => 'Specifinis puslapis', + 'pages_is_template' => 'Puslapio šablonas', + + // Editor Sidebar + 'page_tags' => 'Puslapio žymos', + 'chapter_tags' => 'Skyriaus žymos', + 'book_tags' => 'Knygos žymos', + 'shelf_tags' => 'Lentynų žymos', + 'tag' => 'Žymos', + 'tags' => 'Tags', + 'tag_name' => 'Tag Name', + 'tag_value' => 'Žymos vertė (neprivaloma)', + 'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.", + 'tags_add' => 'Pridėti kitą žymą', + 'tags_remove' => 'Pridėti kitą žymą', + 'attachments' => 'Priedai', + 'attachments_explain' => 'Įkelkite kelis failus arba pridėkite nuorodas savo puslapyje. Jie matomi puslapio šoninėje juostoje.', + 'attachments_explain_instant_save' => 'Pakeitimai čia yra išsaugomi akimirksniu.', + 'attachments_items' => 'Pridėti elementai', + 'attachments_upload' => 'Įkelti failą', + 'attachments_link' => 'Pridėti nuorodą', + 'attachments_set_link' => 'Nustatyti nuorodą', + 'attachments_delete' => 'Ar esate tikri, kad norite ištrinti šį priedą?', + 'attachments_dropzone' => 'Numesti failus arba paspausti čia ir pridėti failą', + 'attachments_no_files' => 'Failai nebuvo įkelti', + 'attachments_explain_link' => 'Jūs galite pridėti nuorodas, jei nenorite įkelti failo. Tai gali būti nuoroda į kitą puslapį arba nuoroda į failą debesyje.', + 'attachments_link_name' => 'Nuorodos pavadinimas', + 'attachment_link' => 'Priedo nuoroda', + 'attachments_link_url' => 'Nuoroda į failą', + 'attachments_link_url_hint' => 'URL į failą', + 'attach' => 'Pridėti', + 'attachments_insert_link' => 'Pridėti priedo nuorodą į puslapį', + 'attachments_edit_file' => 'Redaguoti failą', + 'attachments_edit_file_name' => 'Failo pavadinimas', + 'attachments_edit_drop_upload' => 'Numesti failus arba spausti čia ir atsisiųsti ir perrašyti', + 'attachments_order_updated' => 'Atnaujintas priedų išsidėstymas', + 'attachments_updated_success' => 'Priedų detalės atnaujintos', + 'attachments_deleted' => 'Priedas ištrintas', + 'attachments_file_uploaded' => 'Failas sėkmingai įkeltas', + 'attachments_file_updated' => 'Failas sėkmingai atnaujintas', + 'attachments_link_attached' => 'Nuoroda sėkmingai pridėta puslapyje', + 'templates' => 'Šablonai', + 'templates_set_as_template' => 'Puslapis yra šablonas', + 'templates_explain_set_as_template' => 'Jūs galite nustatyti šį puslapį kaip šabloną, jo turinys bus panaudotas, kuriant kitus puslapius. Kiti naudotojai galės naudotis šiuo šablonu, jei turės peržiūros leidimą šiam puslapiui.', + 'templates_replace_content' => 'Pakeisti puslapio turinį', + 'templates_append_content' => 'Papildyti puslapio turinį', + 'templates_prepend_content' => 'Priklauso nuo puslapio turinio', + + // Profile View + 'profile_user_for_x' => 'Naudotojas :time', + 'profile_created_content' => 'Sukurtas tyrinys', + 'profile_not_created_pages' => ':userName nesukūrė jokio puslapio', + 'profile_not_created_chapters' => ':userName nesukūrė jokio skyriaus', + 'profile_not_created_books' => ':userName nesukūrė jokios knygos', + 'profile_not_created_shelves' => ':userName nesukūrė jokių lentynų', + + // Comments + 'comment' => 'Komentaras', + 'comments' => 'Komentarai', + 'comment_add' => 'Pridėti komentarą', + 'comment_placeholder' => 'Palikite komentarą čia', + 'comment_count' => '{0} Nėra komentarų|{1} 1 komentaras|[2,*] :count komentarai', + 'comment_save' => 'Išsaugoti komentarą', + 'comment_saving' => 'Komentaras išsaugojamas...', + 'comment_deleting' => 'Komentaras ištrinamas...', + 'comment_new' => 'Naujas komentaras', + 'comment_created' => 'Pakomentuota :createDiff', + 'comment_updated' => 'Atnaujinta :updateDiff pagal :username', + 'comment_deleted_success' => 'Komentaras ištrintas', + 'comment_created_success' => 'Komentaras pridėtas', + 'comment_updated_success' => 'Komentaras atnaujintas', + 'comment_delete_confirm' => 'Esate tikri, kad norite ištrinti šį komentarą?', + 'comment_in_reply_to' => 'Atsakydamas į :commentId', + + // Revision + 'revision_delete_confirm' => 'Esate tikri, kad norite ištrinti šią peržiūrą?', + 'revision_restore_confirm' => 'Esate tikri, kad norite atkurti šią peržiūrą? Dabartinis puslapio turinys bus pakeistas.', + 'revision_delete_success' => 'Peržiūra ištrinta', + 'revision_cannot_delete_latest' => 'Negalima išrinti vėliausios peržiūros' +]; diff --git a/resources/lang/lt/errors.php b/resources/lang/lt/errors.php new file mode 100644 index 000000000..c16c37a9b --- /dev/null +++ b/resources/lang/lt/errors.php @@ -0,0 +1,105 @@ + 'Jūs neturite leidimo atidaryti šio puslapio.', + 'permissionJson' => 'Jūs neturite leidimo atlikti prašomo veiksmo.', + + // Auth + 'error_user_exists_different_creds' => 'Naudotojo elektroninis paštas :email jau egzistuoja, bet su kitokiais įgaliojimais.', + 'email_already_confirmed' => 'Elektroninis paštas jau buvo patvirtintas, pabandykite prisijungti.', + 'email_confirmation_invalid' => 'Šis patvirtinimo prieigos raktas negalioja arba jau buvo panaudotas, prašome bandykite vėl registruotis.', + 'email_confirmation_expired' => 'Šis patvirtinimo prieigos raktas baigė galioti, naujas patvirtinimo laiškas jau išsiųstas elektroniniu paštu.', + 'email_confirmation_awaiting' => 'Elektroninio pašto adresą paskyrai reikia patvirtinti', + 'ldap_fail_anonymous' => 'Nepavyko pasiekti LDAP naudojant anoniminį susiejimą', + 'ldap_fail_authed' => 'Nepavyko pasiekti LDAP naudojant išsamią dn ir slaptažodžio informaciją', + 'ldap_extension_not_installed' => 'LDAP PHP išplėtimas neįdiegtas', + 'ldap_cannot_connect' => 'Negalima prisijungti prie LDAP serverio, nepavyko prisijungti', + 'saml_already_logged_in' => 'Jau prisijungta', + 'saml_user_not_registered' => 'Naudotojas :name neužregistruotas ir automatinė registracija yra išjungta', + 'saml_no_email_address' => 'Nerandamas šio naudotojo elektroninio pašto adresas išorinės autentifikavimo sistemos pateiktuose duomenyse', + 'saml_invalid_response_id' => 'Prašymas iš išorinės autentifikavimo sistemos nėra atpažintas proceso, kurį pradėjo ši programa. Naršymas po prisijungimo gali sukelti šią problemą.', + 'saml_fail_authed' => 'Prisijungimas, naudojant :system nepavyko, sistema nepateikė sėkmingo leidimo.', + 'social_no_action_defined' => 'Neapibrėžtas joks veiksmas', + 'social_login_bad_response' => "Error received during :socialAccount login: \n:error", + 'social_account_in_use' => 'Ši :socialAccount paskyra jau yra naudojama, pabandykite prisijungti per :socialAccount pasirinkimą.', + 'social_account_email_in_use' => 'Elektroninis paštas :email jau yra naudojamas. Jei jūs jau turite paskyrą, galite prijungti savo :socialAccount paskyrą iš savo profilio nustatymų.', + 'social_account_existing' => 'Šis :socialAccount jau yra pridėtas prie jūsų profilio.', + 'social_account_already_used_existing' => 'Ši :socialAccount paskyra jau yra naudojama kito naudotojo.', + 'social_account_not_used' => 'Ši :socialAccount paskyra nėra susieta su jokiais naudotojais. Prašome, pridėkite ją į savo profilio nustatymus.', + 'social_account_register_instructions' => 'Jei dar neturite paskyros, galite užregistruoti paskyrą, naudojant :socialAccount pasirinkimą.', + 'social_driver_not_found' => 'Socialinis diskas nerastas', + 'social_driver_not_configured' => 'Jūsų :socialAccount socaliniai nustatymai sukonfigūruoti neteisingai.', + 'invite_token_expired' => 'Ši kvietimo nuoroda baigė galioti. Vietoj to, jūs galite bandyti iš naujo nustatyti savo paskyros slaptažodį.', + + // System + 'path_not_writable' => 'Į failo kelią :filePath negalima įkelti. Įsitikinkite, kad jis yra įrašomas į serverį.', + 'cannot_get_image_from_url' => 'Negalima gauti vaizdo iš :url', + 'cannot_create_thumbs' => 'Serveris negali sukurti miniatiūros. Prašome patikrinkite, ar turite įdiegtą GD PHP plėtinį.', + 'server_upload_limit' => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.', + 'uploaded' => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.', + 'image_upload_error' => 'Įvyko klaida įkeliant vaizdą', + 'image_upload_type_error' => 'Vaizdo tipas, kurį norima įkelti, yra neteisingas', + 'file_upload_timeout' => 'Failo įkėlimo laikas baigėsi', + + // Attachments + 'attachment_not_found' => 'Priedas nerastas', + + // Pages + 'page_draft_autosave_fail' => 'Juodraščio išsaugoti nepavyko. Įsitikinkite, jog turite interneto ryšį prieš išsaugant šį paslapį.', + 'page_custom_home_deletion' => 'Negalima ištrinti šio puslapio, kol jis yra nustatytas kaip pagrindinis puslapis', + + // Entities + 'entity_not_found' => 'Subjektas nerastas', + 'bookshelf_not_found' => 'Knygų lentyna nerasta', + 'book_not_found' => 'Knyga nerasta', + 'page_not_found' => 'Puslapis nerastas', + 'chapter_not_found' => 'Skyrius nerastas', + 'selected_book_not_found' => 'Pasirinkta knyga nerasta', + 'selected_book_chapter_not_found' => 'Pasirinkta knyga ar skyrius buvo nerasti', + 'guests_cannot_save_drafts' => 'Svečiai negali išsaugoti juodraščių', + + // Users + 'users_cannot_delete_only_admin' => 'Negalite ištrinti vienintelio administratoriaus', + 'users_cannot_delete_guest' => 'Negalite ištrinti svečio naudotojo', + + // Roles + 'role_cannot_be_edited' => 'Šio vaidmens negalima redaguoti', + 'role_system_cannot_be_deleted' => 'Šis vaidmuo yra sistemos vaidmuo ir jo negalima ištrinti', + 'role_registration_default_cannot_delete' => 'Šis vaidmuo negali būti ištrintas, kai yra nustatytas kaip numatytasis registracijos vaidmuo', + 'role_cannot_remove_only_admin' => 'Šis naudotojas yra vienintelis naudotojas, kuriam yra paskirtas administratoriaus vaidmuo. Paskirkite administratoriaus vaidmenį kitam naudotojui prieš bandant jį pašalinti.', + + // Comments + 'comment_list' => 'Gaunant komentarus įvyko klaida.', + 'cannot_add_comment_to_draft' => 'Negalite pridėti komentaro juodraštyje', + 'comment_add' => 'Klaido įvyko pridedant/atnaujinant komantarą.', + 'comment_delete' => 'Trinant komentarą įvyko klaida.', + 'empty_comment' => 'Negalite pridėti tuščio komentaro.', + + // Error pages + '404_page_not_found' => 'Puslapis nerastas', + 'sorry_page_not_found' => 'Atleiskite, puslapis, kurio ieškote, nerastas.', + 'sorry_page_not_found_permission_warning' => 'Jei tikėjotės, kad šis puslapis egzistuoja, galbūt neturite leidimo jo peržiūrėti.', + 'image_not_found' => 'Image Not Found', + 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', + 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', + 'return_home' => 'Grįžti į namus', + 'error_occurred' => 'Įvyko klaida', + 'app_down' => ':appName dabar yra apačioje', + 'back_soon' => 'Tai sugrįž greitai', + + // API errors + 'api_no_authorization_found' => 'Užklausoje nerastas įgaliojimo prieigos raktas', + 'api_bad_authorization_format' => 'Užklausoje rastas prieigos raktas, tačiau formatas yra neteisingas', + 'api_user_token_not_found' => 'Pateiktam prieigos raktui nebuvo rastas atitinkamas API prieigos raktas', + 'api_incorrect_token_secret' => 'Pateiktas panaudoto API žetono slėpinys yra neteisingas', + 'api_user_no_api_permission' => 'API prieigos rakto savininkas neturi leidimo daryti API skambučius', + 'api_user_token_expired' => 'Prieigos rakto naudojimas baigė galioti', + + // Settings & Maintenance + 'maintenance_test_email_failure' => 'Siunčiant bandymo email: įvyko klaida', + +]; diff --git a/resources/lang/lt/pagination.php b/resources/lang/lt/pagination.php new file mode 100644 index 000000000..f962f12bb --- /dev/null +++ b/resources/lang/lt/pagination.php @@ -0,0 +1,12 @@ + '« Ankstesnis', + 'next' => 'Kitas »', + +]; diff --git a/resources/lang/lt/passwords.php b/resources/lang/lt/passwords.php new file mode 100644 index 000000000..672620d35 --- /dev/null +++ b/resources/lang/lt/passwords.php @@ -0,0 +1,15 @@ + 'Slaptažodis privalo būti mažiausiai aštuonių simbolių ir atitikti patvirtinimą.', + 'user' => "We can't find a user with that e-mail address.", + 'token' => 'Slaptažodžio nustatymo raktas yra neteisingas šiam elektroninio pašto adresui.', + 'sent' => 'Elektroniu paštu jums atsiuntėme slaptažodžio atkūrimo nuorodą!', + 'reset' => 'Jūsų slaptažodis buvo atkurtas!', + +]; diff --git a/resources/lang/lt/settings.php b/resources/lang/lt/settings.php new file mode 100644 index 000000000..e98f4b493 --- /dev/null +++ b/resources/lang/lt/settings.php @@ -0,0 +1,276 @@ + 'Nustatymai', + 'settings_save' => 'Išsaugoti nustatymus', + 'settings_save_success' => 'Nustatymai išsaugoti', + + // App Settings + 'app_customization' => 'Tinkinimas', + 'app_features_security' => 'Funkcijos ir sauga', + 'app_name' => 'Programos pavadinimas', + 'app_name_desc' => 'Šis pavadinimas yra rodomas antraštėje ir bet kuriuose sistemos siunčiamuose elektroniniuose laiškuose.', + 'app_name_header' => 'Rodyti pavadinimą antraštėje', + 'app_public_access' => 'Vieša prieiga', + 'app_public_access_desc' => 'Įjungus šią parinktį lankytojai, kurie nėra prisijungę, galės pasiekti BookStack egzemplioriaus turinį.', + 'app_public_access_desc_guest' => 'Prieiga viešiems lankytojams gali būti kontroliuojama per "Svečio" naudotoją.', + 'app_public_access_toggle' => 'Leisti viešą prieigą', + 'app_public_viewing' => 'Leisti viešą žiūrėjimą?', + 'app_secure_images' => 'Didesnio saugumo vaizdų įkėlimai', + 'app_secure_images_toggle' => 'Įgalinti didesnio saugumo vaizdų įkėlimus', + 'app_secure_images_desc' => 'Dėl veiklos priežasčių, visi vaizdai yra vieši. Šis pasirinkimas prideda atsitiktinę, sunkiai atspėjamą eilutę prieš vaizdo URL. Įsitikinkite, kad katalogų rodyklės neįgalintos, kad prieiga būtų lengvesnė.', + 'app_editor' => 'Puslapio redaktorius', + 'app_editor_desc' => 'Pasirinkite, kuris redaktorius bus naudojamas visų vartotojų redaguoti puslapiams.', + 'app_custom_html' => 'Pasirinktinis HTL antraštės turinys', + 'app_custom_html_desc' => 'Bet koks čia pridedamas turinys bus prisegamas apačioje kiekvieno puslapio skyriuje. Tai yra patogu svarbesniems stiliams arba pridedant analizės kodą.', + 'app_custom_html_disabled_notice' => 'Pasirinktinis HTML antraštės turinys yra išjungtas šiame nustatymų puslapyje užtikrinti, kad bet kokie negeri pokyčiai galėtų būti anuliuojami.', + 'app_logo' => 'Programos logotipas', + 'app_logo_desc' => 'Šis vaizdas turėtų būti 43px aukščio.
      Dideli vaizdai bus sumažinti.', + 'app_primary_color' => 'Programos pagrindinė spalva', + 'app_primary_color_desc' => 'Nustato pagrindinę spalvą programai, įskaitant reklamjuostę, mygtukus ir nuorodas.', + 'app_homepage' => 'Programos pagrindinis puslapis', + 'app_homepage_desc' => 'Pasirinkite vaizdą rodyti pagrindiniame paslapyje vietoj numatyto vaizdo. Puslapio leidimai yra ignoruojami pasirinktiems puslapiams.', + 'app_homepage_select' => 'Pasirinkti puslapį', + 'app_footer_links' => 'Poraštės nuorodos', + 'app_footer_links_desc' => 'Pridėkite nuorodas, kurias norite pridėti svetainės poraštėje. Jos bus rodomos daugelio puslapių apačioje, įskaitant ir tuos, kurie nereikalauja prisijungimo. Jūs galite naudoti etiktę "trans::", kad naudotis sistemos apibrėžtais vertimais. Pavyzdžiui: naudojimasis "trans::common.privacy_policy" bus pateiktas išverstu tekstu "Privatumo Politika" ir ""trans::common.terms_of_service" bus pateikta išverstu tekstu "Paslaugų Teikimo Sąlygos".', + 'app_footer_links_label' => 'Etiketės nuoroda', + 'app_footer_links_url' => 'Nuoroda URL', + 'app_footer_links_add' => 'Pridėti poraštes nuorodą', + 'app_disable_comments' => 'Išjungti komentarus', + 'app_disable_comments_toggle' => 'Išjungti komentarus', + 'app_disable_comments_desc' => 'Išjungti komentarus visuose programos puslapiuose.
      Esantys komentarai nerodomi.', + + // Color settings + 'content_colors' => 'Turinio spalvos', + 'content_colors_desc' => 'Nustato spalvas visiems elementams puslapio organizacijos herarchijoje. Rekomenduojama pasirinkti spalvas su panačiu šviesumu kaip numatytos spalvos, kad būtų lengviau skaityti.', + 'bookshelf_color' => 'Lentynos spalva', + 'book_color' => 'Knygos spalva', + 'chapter_color' => 'Skyriaus spalva', + 'page_color' => 'Puslapio spalva', + 'page_draft_color' => 'Puslapio juodraščio spalva', + + // Registration Settings + 'reg_settings' => 'Registracija', + 'reg_enable' => 'Įgalinti registraciją', + 'reg_enable_toggle' => 'Įgalinti registraciją', + 'reg_enable_desc' => 'Kai registracija yra įgalinta, naudotojai gali prisiregistruoti kaip programos naudotojai. Registruojantis jiems suteikiamas vienintelis, nematytasis naudotojo vaidmuo.', + 'reg_default_role' => 'Numatytasis naudotojo vaidmuo po registracijos', + 'reg_enable_external_warning' => 'Ankstesnė parinktis nepaisoma, kai išorinis LDAP arba SAML autentifikavimas yra aktyvus. Vartotojo paskyra neegzistuojantiems nariams bus automatiškai sukurta, jei autentifikavimas naudojant naudojamą išorinę sistemą bus sėkmingas.', + 'reg_email_confirmation' => 'Elektroninio pašto patvirtinimas', + 'reg_email_confirmation_toggle' => 'Reikalauja elektroninio pašto patvirtinimo', + 'reg_confirm_email_desc' => 'Jei naudojamas domeno apribojimas, tada elektroninio pašto patvirtinimas bus reikalaujamas ir ši parinktis bus ignoruojama.', + 'reg_confirm_restrict_domain' => 'Domeno apribojimas', + 'reg_confirm_restrict_domain_desc' => 'Įveskite kableliais atskirtą elektroninio pašto domenų, kurių registravimą norite apriboti, sąrašą. Vartotojai išsiųs elektorinį laišką, kad patvirtintumėte jų adresą prieš leidžiant naudotis programa.
      Prisiminkite, kad vartotojai galės pakeisti savo elektroninius paštus po sėkmingos registracijos.', + 'reg_confirm_restrict_domain_placeholder' => 'Nėra jokių apribojimų', + + // Maintenance settings + 'maint' => 'Priežiūra', + 'maint_image_cleanup' => 'Išvalykite vaizdus', + 'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.", + 'maint_delete_images_only_in_revisions' => 'Taip pat ištrinkite vaizdus, kurie yra tik senuose puslapių pataisymuose', + 'maint_image_cleanup_run' => 'Paleisti valymą', + 'maint_image_cleanup_warning' => ':count potencialiai nepanaudoti vaizdai rasti. Ar esate tikri, kad norite ištrinti šiuos vaizdus?', + 'maint_image_cleanup_success' => ':count potencialiai nepanaudoti vaizdai rasti ir ištrinti!', + 'maint_image_cleanup_nothing_found' => 'Nerasta nepanaudotų vaizdų, niekas neištrinta!', + 'maint_send_test_email' => 'Siųsti bandomąjį elektroninį laišką', + 'maint_send_test_email_desc' => 'ai siunčia bandomąjį elektroninį laišką elektroninio pašto adresu, nurodytu jūsų profilyje.', + 'maint_send_test_email_run' => 'Siųsti bandomąjį elektroninį laišką', + 'maint_send_test_email_success' => 'Elektroninis laiškas išsiųstas :address', + 'maint_send_test_email_mail_subject' => 'Bandomasis elektroninis laiškas', + 'maint_send_test_email_mail_greeting' => 'Elektroninio laiško pristatymas veikia!', + 'maint_send_test_email_mail_text' => 'Sveikiname! Kadangi gavote šį elektroninio pašto pranešimą, jūsų elektroninio pašto nustatymai buvo sukonfigūruoti teisingai.', + 'maint_recycle_bin_desc' => 'Ištrintos lentynos, knygos, skyriai ir puslapiai yra perkeliami į šiukšliadėžę tam, kad jie galėtų būti atkurti arba ištrinti visam laikui. Senesni elementai, esantys šiukšliadėžėje, gali būti automatiškai panaikinti po tam tikro laiko priklausomai nuo sistemos konfigūracijos.', + 'maint_recycle_bin_open' => 'Atidaryti šiukšliadėžę', + + // Recycle Bin + 'recycle_bin' => 'Šiukšliadėžė', + 'recycle_bin_desc' => 'Čia gali atkurti elementus, kurie buvo ištrinti arba pasirinkti pašalinti juos iš sistemos visam laikui. Šis sąrašas yra nefiltruotas kaip kitie panašus veiklos sąrašai sistemoje, kuriems yra taikomi leidimo filtrai.', + 'recycle_bin_deleted_item' => 'Ištrintas elementas', + 'recycle_bin_deleted_parent' => 'Parent', + 'recycle_bin_deleted_by' => 'Ištrynė', + 'recycle_bin_deleted_at' => 'Panaikinimo laikas', + 'recycle_bin_permanently_delete' => 'Ištrinti visam laikui', + 'recycle_bin_restore' => 'Atkurti', + 'recycle_bin_contents_empty' => 'Šiukšliadėžė šiuo metu yra tuščia', + 'recycle_bin_empty' => 'Ištuštinti šiukšliadėžę', + 'recycle_bin_empty_confirm' => 'Tai visam laikui sunaikins visus elementus, esančius šiukšliadėžėje, įskaitant kiekvieno elemento turinį. Ar esate tikri, jog norite ištuštinti šiukšliadėžę?', + 'recycle_bin_destroy_confirm' => 'Šis veiksmas visam laikui ištrins šį elementą iš sistemos kartu su bet kuriais elementais įvardintais žemiau ir jūs nebegalėsite atkurti jo bei jo turinio. Ar esate tikri, jog norite visam laikui ištrinti šį elementą?', + 'recycle_bin_destroy_list' => 'Elementai panaikinimui', + 'recycle_bin_restore_list' => 'Elementai atkūrimui', + 'recycle_bin_restore_confirm' => 'Šis veiksmas atkurs ištrintą elementą ir perkels jį atgal į jo originalią vietą. Jei originali vieta buvo ištrinta ir šiuo metu yra šiukšliadėžėje, ji taip pat turės būti atkurta.', + 'recycle_bin_restore_deleted_parent' => 'Pagrindinis elementas buvo ištrintas. Šie elementai liks ištrinti iki tol, kol bus atkurtas pagrindinis elementas.', + 'recycle_bin_restore_parent' => 'Restore Parent', + 'recycle_bin_destroy_notification' => 'Ištrinti :count visus elementus, esančius šiukšliadėžėje.', + 'recycle_bin_restore_notification' => 'Atkurti :count visus elementus, esančius šiukšliadėžėje.', + + // Audit Log + 'audit' => 'Audito seka', + 'audit_desc' => 'Ši audito seka rodo sąrašą veiklų, rastų sistemoje. Šis sąrašas yra nefiltruotas kaip kitie panašus veiklos sąrašai sistemoje, kuriems yra taikomi leidimo filtrai.', + 'audit_event_filter' => 'Įvykio filtras', + 'audit_event_filter_no_filter' => 'Be filtrų', + 'audit_deleted_item' => 'Ištrintas elementas', + 'audit_deleted_item_name' => 'Vardas: :name', + 'audit_table_user' => 'Naudotojas', + 'audit_table_event' => 'Įvykis', + 'audit_table_related' => 'Susijęs elementas arba detalė', + 'audit_table_date' => 'Veiklos data', + 'audit_date_from' => 'Datos seka nuo', + 'audit_date_to' => 'Datos seka iki', + + // Role Settings + 'roles' => 'Vaidmenys', + 'role_user_roles' => 'Naudotojo vaidmenys', + 'role_create' => 'Sukurti naują vaidmenį', + 'role_create_success' => 'Vaidmuo sukurtas sėkmingai', + 'role_delete' => 'Ištrinti vaidmenį', + 'role_delete_confirm' => 'Tai ištrins vaidmenį vardu\':roleName\'.', + 'role_delete_users_assigned' => 'Šis vaidmuo turi :userCount naudotojus priskirtus prie jo. Jeigu norite naudotojus perkelti iš šio vaidmens, pasirinkite naują vaidmenį apačioje.', + 'role_delete_no_migration' => "Don't migrate users", + 'role_delete_sure' => 'Ar esate tikri, jog norite ištrinti šį vaidmenį?', + 'role_delete_success' => 'Vaidmuo ištrintas sėkmingai', + 'role_edit' => 'Redaguoti vaidmenį', + 'role_details' => 'Vaidmens detalės', + 'role_name' => 'Vaidmens pavadinimas', + 'role_desc' => 'Trumpas vaidmens aprašymas', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', + 'role_external_auth_id' => 'Išorinio autentifikavimo ID', + 'role_system' => 'Sistemos leidimai', + 'role_manage_users' => 'Tvarkyti naudotojus', + 'role_manage_roles' => 'Tvarkyti vaidmenis ir vaidmenų leidimus', + 'role_manage_entity_permissions' => 'Tvarkyti visus knygų, skyrių ir puslapių leidimus', + 'role_manage_own_entity_permissions' => 'Tvarkyti savo knygos, skyriaus ir puslapių leidimus', + 'role_manage_page_templates' => 'Tvarkyti puslapių šablonus', + 'role_access_api' => 'Gauti prieigą prie sistemos API', + 'role_manage_settings' => 'Tvarkyti programos nustatymus', + 'role_export_content' => 'Export content', + 'role_asset' => 'Nuosavybės leidimai', + 'roles_system_warning' => 'Būkite sąmoningi, kad prieiga prie bet kurio iš trijų leidimų viršuje gali leisti naudotojui pakeisti jų pačių privilegijas arba kitų privilegijas sistemoje. Paskirkite vaidmenis su šiais leidimais tik patikimiems naudotojams.', + 'role_asset_desc' => 'Šie leidimai kontroliuoja numatytą prieigą į nuosavybę, esančią sistemoje. Knygų, skyrių ir puslapių leidimai nepaisys šių leidimų.', + 'role_asset_admins' => 'Administratoriams automatiškai yra suteikiama prieiga prie viso turinio, tačiau šie pasirinkimai gali rodyti arba slėpti vartotojo sąsajos parinktis.', + 'role_all' => 'Visi', + 'role_own' => 'Nuosavi', + 'role_controlled_by_asset' => 'Kontroliuojami nuosavybės, į kurią yra įkelti', + 'role_save' => 'Išsaugoti vaidmenį', + 'role_update_success' => 'Vaidmuo atnaujintas sėkmingai', + 'role_users' => 'Naudotojai šiame vaidmenyje', + 'role_users_none' => 'Šiuo metu prie šio vaidmens nėra priskirta naudotojų', + + // Users + 'users' => 'Naudotojai', + 'user_profile' => 'Naudotojo profilis', + 'users_add_new' => 'Pridėti naują naudotoją', + 'users_search' => 'Ieškoti naudotojų', + 'users_latest_activity' => 'Naujausia veikla', + 'users_details' => 'Naudotojo detalės', + 'users_details_desc' => 'Nustatykite rodomąjį vardą ir elektroninio pašto adresą šiam naudotojui. Šis elektroninio pašto adresas bus naudojamas prisijungimui prie aplikacijos.', + 'users_details_desc_no_email' => 'Nustatykite rodomąjį vardą šiam naudotojui, kad kiti galėtų jį atpažinti.', + 'users_role' => 'Naudotojo vaidmenys', + 'users_role_desc' => 'Pasirinkite, prie kokių vaidmenų bus priskirtas šis naudotojas. Jeigu naudotojas yra priskirtas prie kelių vaidmenų, leidimai iš tų vaidmenų susidės ir jie gaus visus priskirtų vaidmenų gebėjimus.', + 'users_password' => 'Naudotojo slaptažodis', + 'users_password_desc' => 'Susikurkite slaptažodį, kuris bus naudojamas prisijungti prie aplikacijos. Slaptažodis turi būti bent 6 simbolių ilgio.', + 'users_send_invite_text' => 'Jūs galite pasirinkti nusiųsti šiam naudotojui kvietimą elektroniniu paštu, kuris leistų jiems patiems susikurti slaptažodį. Priešingu atveju slaptažodį galite sukurti patys.', + 'users_send_invite_option' => 'Nusiųsti naudotojui kvietimą elektroniniu paštu', + 'users_external_auth_id' => 'Išorinio autentifikavimo ID', + 'users_external_auth_id_desc' => 'Tai yra ID, naudojamas norint suderinti šį naudotoją bendraujant su jūsų išorinio autentifikavimo sistema.', + 'users_password_warning' => 'Užpildykite laukelį apačioje tik tuo atveju, jeigu norite pakeisti savo slaptažodį.', + 'users_system_public' => 'Šis naudotojas atstovauja svečius, kurie aplanko jūsų egzempliorių. Jis negali būti naudojamas prisijungimui, tačiau yra priskiriamas automatiškai.', + 'users_delete' => 'Ištrinti naudotoją', + 'users_delete_named' => 'Ištrinti naudotoją :userName', + 'users_delete_warning' => 'Tai pilnai ištrins šį naudotoją vardu \':userName\' iš sistemos.', + 'users_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį naudotoją?', + 'users_migrate_ownership' => 'Perkelti nuosavybę', + 'users_migrate_ownership_desc' => 'Pasirinkite naudotoją, jeigu norite, kad kitas naudotojas taptų visų elementų, šiuo metu priklausančių šiam naudotojui, savininku.', + 'users_none_selected' => 'Naudotojas nepasirinktas', + 'users_delete_success' => 'Naudotojas sėkmingai pašalintas', + 'users_edit' => 'Redaguoti naudotoją', + 'users_edit_profile' => 'Redaguoti profilį', + 'users_edit_success' => 'Naudotojas sėkmingai atnaujintas', + 'users_avatar' => 'Naudotojo pseudoportretas', + 'users_avatar_desc' => 'Pasirinkite nuotrauką, pavaizduojančią šį naudotoją. Nuotrauka turi būti maždaug 256px kvadratas.', + 'users_preferred_language' => 'Norima kalba', + 'users_preferred_language_desc' => 'Ši parinktis pakeis kalbą, naudojamą naudotojo sąsajoje aplikacijoje. Tai neturės įtakos jokiam vartotojo sukurtam turiniui.', + 'users_social_accounts' => 'Socialinės paskyros', + 'users_social_accounts_info' => 'Čia galite susieti savo kitas paskyras greitesniam ir lengvesniam prisijungimui. Atjungus paskyrą čia neatšaukiama anksčiau leista prieiga. Atšaukite prieigą iš profilio nustatymų prijungtoje socialinėje paskyroje.', + 'users_social_connect' => 'Susieti paskyrą', + 'users_social_disconnect' => 'Atskirti paskyrą', + 'users_social_connected' => ':socialAccount paskyra buvo sėkmingai susieta su jūsų profiliu.', + 'users_social_disconnected' => ':socialAccount paskyra buvo sėkmingai atskirta nuo jūsu profilio.', + 'users_api_tokens' => 'API sąsajos prieigos raktai', + 'users_api_tokens_none' => 'Jokie API sąsajos prieigos raktai nebuvo sukurti šiam naudotojui', + 'users_api_tokens_create' => 'Sukurti prieigos raktą', + 'users_api_tokens_expires' => 'Baigia galioti', + 'users_api_tokens_docs' => 'API dokumentacija', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', + + // API Tokens + 'user_api_token_create' => 'Sukurti API sąsajos prieigos raktą', + 'user_api_token_name' => 'Pavadinimas', + 'user_api_token_name_desc' => 'Suteikite savo prieigos raktui perskaitomą pavadinimą kaip priminimą ateičiai apie jo numatytą tikslą.', + 'user_api_token_expiry' => 'Galiojimo laikas', + 'user_api_token_expiry_desc' => 'Nustatykite datą kada šis prieigos raktas baigs galioti. Po šios datos, prašymai, atlikti naudojant šį prieigos raktą daugiau nebeveiks. Jeigu šį laukelį paliksite tuščią, galiojimo laikas bus nustatytas 100 metų į ateitį.', + 'user_api_token_create_secret_message' => 'Iš karto sukūrus šį prieigos raktą, bus sukurtas ir rodomas "Priegos rakto ID" ir "Prieigos rakto slėpinys". Prieigos rakto slėpinys bus rodomas tik vieną kartą, todėl būtinai nukopijuokite jį kur nors saugioje vietoje.', + 'user_api_token_create_success' => 'API sąsajos prieigos raktas sėkmingai sukurtas', + 'user_api_token_update_success' => 'API sąsajos prieigos raktas sėkmingai atnaujintas', + 'user_api_token' => 'API sąsajos prieigos raktas', + 'user_api_token_id' => 'Prieigos rakto ID', + 'user_api_token_id_desc' => 'Tai neredaguojamas sistemos sugeneruotas identifikatorius šiam prieigos raktui, kurį reikės pateikti API užklausose.', + 'user_api_token_secret' => 'Priegos rakto slėpinys', + 'user_api_token_secret_desc' => 'Tai yra sistemos sukurtas šio priegos rakto slėpinys, kurią reikės pateikti API užklausose. Tai bus rodoma tik šį kartą, todėl nukopijuokite šią vertę į saugią vietą.', + 'user_api_token_created' => 'Prieigos raktas sukurtas :timeAgo', + 'user_api_token_updated' => 'Prieigos raktas atnaujintas :timeAgo', + 'user_api_token_delete' => 'Ištrinti prieigos raktą', + 'user_api_token_delete_warning' => 'Tai pilnai ištrins šį API sąsajos prieigos raktą pavadinimu \':tokenName\' iš sistemos.', + 'user_api_token_delete_confirm' => 'Ar esate tikri, jog norite ištrinti šį API sąsajos prieigos raktą?', + 'user_api_token_delete_success' => 'API sąsajos prieigos raktas sėkmingai ištrintas', + + //! If editing translations files directly please ignore this in all + //! languages apart from en. Content will be auto-copied from en. + //!//////////////////////////////// + 'language_select' => [ + 'en' => 'English', + 'ar' => 'العربية', + 'bg' => 'Bǎlgarski', + 'bs' => 'Bosanski', + 'ca' => 'Català', + 'cs' => 'Česky', + 'da' => 'Dansk', + 'de' => 'Deutsch (Sie)', + 'de_informal' => 'Deutsch (Du)', + 'es' => 'Español', + 'es_AR' => 'Español Argentina', + 'fr' => 'Français', + 'he' => 'עברית', + 'hr' => 'Hrvatski', + 'hu' => 'Magyar', + 'id' => 'Bahasa Indonesia', + 'it' => 'Italian', + 'ja' => '日本語', + 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', + 'lv' => 'Latviešu Valoda', + 'nl' => 'Nederlands', + 'nb' => 'Norsk (Bokmål)', + 'pl' => 'Polski', + 'pt' => 'Português', + 'pt_BR' => 'Português do Brasil', + 'ru' => 'Русский', + 'sk' => 'Slovensky', + 'sl' => 'Slovenščina', + 'sv' => 'Svenska', + 'tr' => 'Türkçe', + 'uk' => 'Українська', + 'vi' => 'Tiếng Việt', + 'zh_CN' => '简体中文', + 'zh_TW' => '繁體中文', + ] + //!//////////////////////////////// +]; diff --git a/resources/lang/lt/validation.php b/resources/lang/lt/validation.php new file mode 100644 index 000000000..8fa9234cc --- /dev/null +++ b/resources/lang/lt/validation.php @@ -0,0 +1,116 @@ + ':attribute turi būti priimtas.', + 'active_url' => ':attribute nėra tinkamas URL.', + 'after' => ':attribute turi būti data po :date.', + 'alpha' => ':attribute turi būti sudarytis tik iš raidžių.', + 'alpha_dash' => ':attribute turi būti sudarytas tik iš raidžių, skaičių, brūkšnelių ir pabraukimų.', + 'alpha_num' => ':attribute turi būti sudarytas tik iš raidžių ir skaičių.', + 'array' => ':attribute turi būti masyvas.', + 'backup_codes' => 'The provided code is not valid or has already been used.', + 'before' => ':attribute turi būti data anksčiau negu :date.', + 'between' => [ + 'numeric' => ':attribute turi būti tarp :min ir :max.', + 'file' => ':attribute turi būti tarp :min ir :max kilobaitų.', + 'string' => ':attribute turi būti tarp :min ir :max simbolių.', + 'array' => ':attribute turi turėti tarp :min ir :max elementų.', + ], + 'boolean' => ':attribute laukas turi būti tiesa arba melas.', + 'confirmed' => ':attribute patvirtinimas nesutampa.', + 'date' => ':attribute nėra tinkama data.', + 'date_format' => ':attribute neatitinka formato :format.', + 'different' => ':attribute ir :other turi būti skirtingi.', + 'digits' => ':attribute turi būti :digits skaitmenų.', + 'digits_between' => ':attribute turi būti tarp :min ir :max skaitmenų.', + 'email' => ':attribute turi būti tinkamas elektroninio pašto adresas.', + 'ends_with' => ':attribute turi pasibaigti vienu iš šių: :values', + 'filled' => ':attribute laukas yra privalomas.', + 'gt' => [ + 'numeric' => ':attribute turi būti didesnis negu :value.', + 'file' => ':attribute turi būti didesnis negu :value kilobaitai.', + 'string' => ':attribute turi būti didesnis negu :value simboliai.', + 'array' => ':attribute turi turėti daugiau negu :value elementus.', + ], + 'gte' => [ + 'numeric' => ':attribute turi būti didesnis negu arba lygus :value.', + 'file' => ':attribute turi būti didesnis negu arba lygus :value kilobaitams.', + 'string' => ':attribute turi būti didesnis negu arba lygus :value simboliams.', + 'array' => ':attribute turi turėti :value elementus arba daugiau.', + ], + 'exists' => 'Pasirinktas :attribute yra klaidingas.', + 'image' => ':attribute turi būti paveikslėlis.', + 'image_extension' => ':attribute turi būti tinkamas ir palaikomas vaizdo plėtinys.', + 'in' => 'Pasirinktas :attribute yra klaidingas.', + 'integer' => ':attribute turi būti sveikasis skaičius.', + 'ip' => ':attribute turi būti tinkamas IP adresas.', + 'ipv4' => ':attribute turi būti tinkamas IPv4 adresas.', + 'ipv6' => ':attribute turi būti tinkamas IPv6 adresas.', + 'json' => ':attribute turi būti tinkama JSON eilutė.', + 'lt' => [ + 'numeric' => ':attribute turi būti mažiau negu :value.', + 'file' => ':attribute turi būti mažiau negu :value kilobaitai.', + 'string' => ':attribute turi būti mažiau negu :value simboliai.', + 'array' => ':attribute turi turėti mažiau negu :value elementus.', + ], + 'lte' => [ + 'numeric' => ':attribute turi būti mažiau arba lygus :value.', + 'file' => ':attribute turi būti mažiau arba lygus :value kilobaitams.', + 'string' => ':attribute turi būti mažiau arba lygus :value simboliams.', + 'array' => ':attribute negali turėti daugiau negu :value elementų.', + ], + 'max' => [ + 'numeric' => ':attribute negali būti didesnis negu :max.', + 'file' => ':attribute negali būti didesnis negu :max kilobaitai.', + 'string' => ':attribute negali būti didesnis negu :max simboliai.', + 'array' => ':attribute negali turėti daugiau negu :max elementų.', + ], + 'mimes' => ':attribute turi būti tipo failas: :values.', + 'min' => [ + 'numeric' => ':attribute turi būti mažiausiai :min.', + 'file' => ':attribute turi būti mažiausiai :min kilobaitų.', + 'string' => ':attribute turi būti mažiausiai :min simbolių.', + 'array' => ':attribute turi turėti mažiausiai :min elementus.', + ], + 'not_in' => 'Pasirinktas :attribute yra klaidingas.', + 'not_regex' => ':attribute formatas yra klaidingas.', + 'numeric' => ':attribute turi būti skaičius.', + 'regex' => ':attribute formatas yra klaidingas.', + 'required' => ':attribute laukas yra privalomas.', + 'required_if' => ':attribute laukas yra privalomas kai :other yra :value.', + 'required_with' => ':attribute laukas yra privalomas kai :values yra.', + 'required_with_all' => ':attribute laukas yra privalomas kai :values yra.', + 'required_without' => ':attribute laukas yra privalomas kai nėra :values.', + 'required_without_all' => ':attribute laukas yra privalomas kai nėra nei vienos :values.', + 'same' => ':attribute ir :other turi sutapti.', + 'safe_url' => 'Pateikta nuoroda gali būti nesaugi.', + 'size' => [ + 'numeric' => ':attribute turi būti :size.', + 'file' => ':attribute turi būti :size kilobaitų.', + 'string' => ':attribute turi būti :size simbolių.', + 'array' => ':attribute turi turėti :size elementus.', + ], + 'string' => ':attribute turi būti eilutė.', + 'timezone' => ':attribute turi būti tinkama zona.', + 'totp' => 'The provided code is not valid or has expired.', + 'unique' => ':attribute jau yra paimtas.', + 'url' => ':attribute formatas yra klaidingas.', + 'uploaded' => 'Šis failas negali būti įkeltas. Serveris gali nepriimti tokio dydžio failų.', + + // Custom validation lines + 'custom' => [ + 'password-confirm' => [ + 'required_with' => 'Reikalingas slaptažodžio patvirtinimas', + ], + ], + + // Custom validation attributes + 'attributes' => [], +]; diff --git a/resources/lang/lv/activities.php b/resources/lang/lv/activities.php index e424efa1d..bf8a9e288 100644 --- a/resources/lang/lv/activities.php +++ b/resources/lang/lv/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" ir pievienots jūsu favorītiem', 'favourite_remove_notification' => '":name" ir izņemts no jūsu favorītiem', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'komentēts', 'permissions_update' => 'atjaunoja atļaujas', diff --git a/resources/lang/lv/auth.php b/resources/lang/lv/auth.php index dc84a2d97..f55423ec5 100644 --- a/resources/lang/lv/auth.php +++ b/resources/lang/lv/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Sveicināti :appName!', 'user_invite_page_text' => 'Lai pabeigtu profila izveidi un piekļūtu :appName ir jāizveido parole.', 'user_invite_page_confirm_button' => 'Apstiprināt paroli', - 'user_invite_success' => 'Parole iestatīta, tagad varat piekļūt :appName!' + 'user_invite_success' => 'Parole iestatīta, tagad varat piekļūt :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/lv/common.php b/resources/lang/lv/common.php index f99419c93..1694e3f92 100644 --- a/resources/lang/lv/common.php +++ b/resources/lang/lv/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Atiestatīt', 'remove' => 'Noņemt', 'add' => 'Pievienot', + 'configure' => 'Configure', 'fullscreen' => 'Pilnekrāns', 'favourite' => 'Pievienot favorītiem', 'unfavourite' => 'Noņemt no favorītiem', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nav skatāmu darbību', 'no_items' => 'Vienumi nav pieejami', 'back_to_top' => 'Uz augšu', + 'skip_to_main_content' => 'Pāriet uz saturu', 'toggle_details' => 'Rādīt aprakstu', 'toggle_thumbnails' => 'Iezīmēt sīkatēlus', 'details' => 'Sīkāka informācija', diff --git a/resources/lang/lv/entities.php b/resources/lang/lv/entities.php index 0a01b9bf3..7a658c7d9 100644 --- a/resources/lang/lv/entities.php +++ b/resources/lang/lv/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Pilna satura web fails', 'export_pdf' => 'PDF fails', 'export_text' => 'Vienkāršs teksta fails', + 'export_md' => 'Markdown fails', // Permissions and restrictions 'permissions' => 'Atļaujas', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Grāmatplaukta atļaujas', 'shelves_permissions_updated' => 'Grāmatplaukta atļaujas atjauninātas', 'shelves_permissions_active' => 'Grāmatplaukta atļaujas ir aktīvas', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopēt grāmatplaukta atļaujas uz grāmatām', 'shelves_copy_permissions' => 'Kopēt atļaujas', 'shelves_copy_permissions_explain' => 'Šis piemēros pašreizējās grāmatplaukta piekļuves tiesības visām tajā esošajām grāmatām. Pirms ieslēgšanas pārliecinieties, ka ir saglabātas izmaiņas grāmatplaukta piekļuves tiesībām.', diff --git a/resources/lang/lv/settings.php b/resources/lang/lv/settings.php index dc00a23b3..88614da5b 100644 --- a/resources/lang/lv/settings.php +++ b/resources/lang/lv/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Miskaste', 'recycle_bin_desc' => 'Te jūs varat atjaunot dzēstās vienības vai arī izdzēst tās no sistēmas pilnībā. Šis saraksts nav filtrēts atšķirībā no līdzīgiem darbību sarakstiem sistēmā, kur ir piemēroti piekļuves tiesību filtri.', 'recycle_bin_deleted_item' => 'Dzēsta vienība', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Izdzēsa', 'recycle_bin_deleted_at' => 'Dzēšanas laiks', 'recycle_bin_permanently_delete' => 'Neatgriezeniski izdzēst', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Atjaunojamās vienības', 'recycle_bin_restore_confirm' => 'Šī darbība atjaunos dzēsto vienību, tai skaitā visus tai pakārtotos elementus, uz tās sākotnējo atrašanās vietu. Ja sākotnējā atrašanās vieta ir izdzēsta un atrodas miskastē, būs nepieciešams atjaunot arī to.', 'recycle_bin_restore_deleted_parent' => 'Šo elementu saturošā vienība arī ir dzēsta. Tas paliks dzēsts līdz šī saturošā vienība arī ir atjaunota.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Dzēstas kopā :count vienības no miskastes.', 'recycle_bin_restore_notification' => 'Atjaunotas kopā :count vienības no miskastes.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Informācija par grupu', 'role_name' => 'Grupas nosaukums', 'role_desc' => 'Īss grupas apaksts', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Ārējais autentifikācijas ID', 'role_system' => 'Sistēmas atļaujas', 'role_manage_users' => 'Pārvaldīt lietotājus', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Pārvaldīt lapas veidnes', 'role_access_api' => 'Piekļūt sistēmas API', 'role_manage_settings' => 'Pārvaldīt iestatījumus', + 'role_export_content' => 'Export content', 'role_asset' => 'Resursa piekļuves tiesības', 'roles_system_warning' => 'Jebkuras no trīs augstāk redzamajām atļaujām dod iespēju lietotājam mainīt savas un citu lietotāju sistēmas atļaujas. Pievieno šīs grupu atļaujas tikai tiem lietotājiem, kuriem uzticies.', 'role_asset_desc' => 'Šīs piekļuves tiesības kontrolē noklusēto piekļuvi sistēmas resursiem. Grāmatām, nodaļām un lapām norādītās tiesības būs pārākas par šīm.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Izveidot žetonu', 'users_api_tokens_expires' => 'Derīguma termiņš', 'users_api_tokens_docs' => 'API dokumentācija', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Izveidot API žetonu', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/lv/validation.php b/resources/lang/lv/validation.php index 6de2b3973..c013eda48 100644 --- a/resources/lang/lv/validation.php +++ b/resources/lang/lv/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute var saturēt tikai burtus, ciparus, domuzīmes un apakš svītras.', 'alpha_num' => ':attribute var saturēt tikai burtus un ciparus.', 'array' => ':attribute ir jābūt masīvam.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute jābūt datumam pirms :date.', 'between' => [ 'numeric' => ':attribute jābūt starp :min un :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute jābūt teksta virknei.', 'timezone' => ':attribute jābūt derīgai zonai.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute jau ir aizņemts.', 'url' => ':attribute formāts nav derīgs.', 'uploaded' => 'Fails netika ielādēts. Serveris nevar pieņemt šāda izmēra failus.', diff --git a/resources/lang/nb/activities.php b/resources/lang/nb/activities.php index 12d0dba62..7313f37f1 100644 --- a/resources/lang/nb/activities.php +++ b/resources/lang/nb/activities.php @@ -45,8 +45,12 @@ return [ 'bookshelf_delete_notification' => 'Bokhyllen ble slettet', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '«:name» ble lagt til i dine favoritter', + 'favourite_remove_notification' => '«:name» ble fjernet fra dine favoritter', + + // MFA + 'mfa_setup_method_notification' => 'Flerfaktor-metoden ble konfigurert', + 'mfa_remove_method_notification' => 'Flerfaktor-metoden ble fjernet', // Other 'commented_on' => 'kommenterte på', diff --git a/resources/lang/nb/auth.php b/resources/lang/nb/auth.php index ae145d28b..4c1f55577 100644 --- a/resources/lang/nb/auth.php +++ b/resources/lang/nb/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Velkommen til :appName!', 'user_invite_page_text' => 'For å fullføre prosessen må du oppgi et passord som sikrer din konto på :appName for fremtidige besøk.', 'user_invite_page_confirm_button' => 'Bekreft passord', - 'user_invite_success' => 'Passordet er angitt, du kan nå bruke :appName!' + 'user_invite_success' => 'Passordet er angitt, du kan nå bruke :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Konfigurer flerfaktor-autentisering', + 'mfa_setup_desc' => 'Konfigurer flerfaktor-autentisering som et ekstra lag med sikkerhet for brukerkontoen din.', + 'mfa_setup_configured' => 'Allerede konfigurert', + 'mfa_setup_reconfigure' => 'Omkonfigurer', + 'mfa_setup_remove_confirmation' => 'Er du sikker på at du vil deaktivere denne flerfaktor-autentiseringsmetoden?', + 'mfa_setup_action' => 'Konfigurasjon', + 'mfa_backup_codes_usage_limit_warning' => 'Du har mindre enn 5 sikkerhetskoder igjen; vennligst generer og lagre ett nytt sett før du går tom for koder, for å unngå å bli låst ute av kontoen din.', + 'mfa_option_totp_title' => 'Mobilapplikasjon', + 'mfa_option_totp_desc' => 'For å bruke flerfaktorautentisering trenger du en mobilapplikasjon som støtter TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Sikkerhetskoder', + 'mfa_option_backup_codes_desc' => 'Lagre sikkerhetskoder til engangsbruk på et trygt sted, disse kan du bruke for å verifisere identiteten din.', + 'mfa_gen_confirm_and_enable' => 'Bekreft og aktiver', + 'mfa_gen_backup_codes_title' => 'Konfigurasjon av sikkerhetskoder', + 'mfa_gen_backup_codes_desc' => 'Lagre nedeforstående liste med koder på et trygt sted. Når du skal ha tilgang til systemet kan du bruke en av disse som en faktor under innlogging.', + 'mfa_gen_backup_codes_download' => 'Last ned koder', + 'mfa_gen_backup_codes_usage_warning' => 'Hver kode kan kun brukes en gang', + 'mfa_gen_totp_title' => 'Oppsett for mobilapplikasjon', + 'mfa_gen_totp_desc' => 'For å bruke flerfaktorautentisering trenger du en mobilapplikasjon som støtter TOTP-teknologien, slik som Google Authenticator, Authy eller Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan QR-koden nedenfor med valgt TOTP-applikasjon for å starte.', + 'mfa_gen_totp_verify_setup' => 'Bekreft oppsett', + 'mfa_gen_totp_verify_setup_desc' => 'Bekreft at oppsettet fungerer ved å skrive inn koden fra TOTP-applikasjonen i boksen nedenfor:', + 'mfa_gen_totp_provide_code_here' => 'Skriv inn den genererte koden her', + 'mfa_verify_access' => 'Bekreft tilgang', + 'mfa_verify_access_desc' => 'Brukerkontoen din krever at du bekrefter din identitet med en ekstra autentiseringsfaktor før du får tilgang. Bekreft identiteten med en av dine konfigurerte metoder for å fortsette.', + 'mfa_verify_no_methods' => 'Ingen metoder er konfigurert', + 'mfa_verify_no_methods_desc' => 'Ingen flerfaktorautentiseringsmetoder er satt opp for din konto. Du må sette opp minst en metode for å få tilgang.', + 'mfa_verify_use_totp' => 'Bekreft med mobilapplikasjon', + 'mfa_verify_use_backup_codes' => 'Bekreft med sikkerhetskode', + 'mfa_verify_backup_code' => 'Sikkerhetskode', + 'mfa_verify_backup_code_desc' => 'Skriv inn en av dine ubrukte sikkerhetskoder under:', + 'mfa_verify_backup_code_enter_here' => 'Skriv inn sikkerhetskode her', + 'mfa_verify_totp_desc' => 'Skriv inn koden, generert ved hjelp av mobilapplikasjonen, nedenfor:', + 'mfa_setup_login_notification' => 'Flerfaktorautentisering er konfigurert, vennligst logg inn på nytt med denne metoden.', ]; \ No newline at end of file diff --git a/resources/lang/nb/common.php b/resources/lang/nb/common.php index 3aadd805a..8ba4e7474 100644 --- a/resources/lang/nb/common.php +++ b/resources/lang/nb/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Nullstill', 'remove' => 'Fjern', 'add' => 'Legg til', + 'configure' => 'Konfigurer', 'fullscreen' => 'Fullskjerm', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Favorisér', + 'unfavourite' => 'Avfavorisér', + 'next' => 'Neste', + 'previous' => 'Forrige', // Sort Options 'sort_options' => 'Sorteringsalternativer', @@ -51,7 +52,7 @@ return [ 'sort_ascending' => 'Stigende sortering', 'sort_descending' => 'Synkende sortering', 'sort_name' => 'Navn', - 'sort_default' => 'Default', + 'sort_default' => 'Standard', 'sort_created_at' => 'Dato opprettet', 'sort_updated_at' => 'Dato oppdatert', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Ingen aktivitet å vise', 'no_items' => 'Ingen ting å vise', 'back_to_top' => 'Hopp til toppen', + 'skip_to_main_content' => 'Gå til hovedinnhold', 'toggle_details' => 'Vis/skjul detaljer', 'toggle_thumbnails' => 'Vis/skjul miniatyrbilder', 'details' => 'Detaljer', @@ -69,7 +71,7 @@ return [ 'breadcrumb' => 'Brødsmuler', // Header - 'header_menu_expand' => 'Expand Header Menu', + 'header_menu_expand' => 'Utvid toppmeny', 'profile_menu' => 'Profilmeny', 'view_profile' => 'Vis profil', 'edit_profile' => 'Endre Profile', @@ -78,9 +80,9 @@ return [ // Layout tabs 'tab_info' => 'Informasjon', - 'tab_info_label' => 'Tab: Show Secondary Information', + 'tab_info_label' => 'Fane: Vis tilleggsinfo', 'tab_content' => 'Innhold', - 'tab_content_label' => 'Tab: Show Primary Content', + 'tab_content_label' => 'Fane: Vis hovedinnhold', // Email Content 'email_action_help' => 'Om du har problemer med å trykke på «:actionText»-knappen, bruk nettadressen under for å gå direkte dit:', @@ -88,6 +90,6 @@ return [ // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Privacy Policy', - 'terms_of_service' => 'Terms of Service', + 'privacy_policy' => 'Personvernregler', + 'terms_of_service' => 'Bruksvilkår', ]; diff --git a/resources/lang/nb/entities.php b/resources/lang/nb/entities.php index 0b3d1b416..a50aa71a8 100644 --- a/resources/lang/nb/entities.php +++ b/resources/lang/nb/entities.php @@ -27,8 +27,8 @@ return [ 'images' => 'Bilder', 'my_recent_drafts' => 'Mine nylige utkast', 'my_recently_viewed' => 'Mine nylige visninger', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => 'Mine mest viste favoritter', + 'my_favourites' => 'Mine favoritter', 'no_pages_viewed' => 'Du har ikke sett på noen sider', 'no_pages_recently_created' => 'Ingen sider har nylig blitt opprettet', 'no_pages_recently_updated' => 'Ingen sider har nylig blitt oppdatert', @@ -36,6 +36,7 @@ return [ 'export_html' => 'Nettside med alt', 'export_pdf' => 'PDF Fil', 'export_text' => 'Tekstfil', + 'export_md' => 'Markdownfil', // Permissions and restrictions 'permissions' => 'Tilganger', @@ -62,7 +63,7 @@ return [ 'search_permissions_set' => 'Tilganger er angitt', 'search_created_by_me' => 'Opprettet av meg', 'search_updated_by_me' => 'Oppdatert av meg', - 'search_owned_by_me' => 'Owned by me', + 'search_owned_by_me' => 'Eid av meg', 'search_date_options' => 'Datoalternativer', 'search_updated_before' => 'Oppdatert før', 'search_updated_after' => 'Oppdatert etter', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Tilganger til hylla', 'shelves_permissions_updated' => 'Hyllas tilganger er oppdatert', 'shelves_permissions_active' => 'Hyllas tilganger er aktive', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla', 'shelves_copy_permissions' => 'Kopier tilganger', 'shelves_copy_permissions_explain' => 'Dette vil angi gjeldende tillatelsesinnstillinger for denne bokhyllen på alle bøkene som finnes på den. Før du aktiverer, må du forsikre deg om at endringer i tillatelsene til denne bokhyllen er lagret.', diff --git a/resources/lang/nb/errors.php b/resources/lang/nb/errors.php index 971dbf1ca..4713be3a4 100644 --- a/resources/lang/nb/errors.php +++ b/resources/lang/nb/errors.php @@ -83,9 +83,9 @@ return [ '404_page_not_found' => 'Siden finnes ikke', 'sorry_page_not_found' => 'Beklager, siden du leter etter ble ikke funnet.', 'sorry_page_not_found_permission_warning' => 'Hvis du forventet at denne siden skulle eksistere, har du kanskje ikke tillatelse til å se den.', - 'image_not_found' => 'Image Not Found', - 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', - 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', + 'image_not_found' => 'Bildet ble ikke funnet', + 'image_not_found_subtitle' => 'Beklager, bildefilen du ser etter ble ikke funnet.', + 'image_not_found_details' => 'Om du forventet at dette bildet skal eksistere, er det mulig det er slettet.', 'return_home' => 'Gå til hovedside', 'error_occurred' => 'En feil oppsto', 'app_down' => ':appName er nede for øyeblikket', diff --git a/resources/lang/nb/settings.php b/resources/lang/nb/settings.php index 1451cff70..6410e9082 100644 --- a/resources/lang/nb/settings.php +++ b/resources/lang/nb/settings.php @@ -37,11 +37,11 @@ return [ 'app_homepage' => 'Applikasjonens hjemmeside', 'app_homepage_desc' => 'Velg en visning som skal vises på hjemmesiden i stedet for standardvisningen. Sidetillatelser ignoreres for utvalgte sider.', 'app_homepage_select' => 'Velg en side', - 'app_footer_links' => 'Footer Links', - 'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".', - 'app_footer_links_label' => 'Link Label', - 'app_footer_links_url' => 'Link URL', - 'app_footer_links_add' => 'Add Footer Link', + 'app_footer_links' => 'Fotlenker', + 'app_footer_links_desc' => 'Legg til fotlenker i sidens fotområde. Disse vil vises nederst på de fleste sider, inkludert sider som ikke krever innlogging. Du kan bruke «trans::» etiketter for system-definerte oversettelser. For eksempel: Bruk «trans::common.privacy_policy» for å vise teksten «Personvernregler» og «trans::common.terms_of_service» for å vise teksten «Bruksvilkår».', + 'app_footer_links_label' => 'Lenketekst', + 'app_footer_links_url' => 'Lenke', + 'app_footer_links_add' => 'Legg til fotlenke', 'app_disable_comments' => 'Deaktiver kommentarer', 'app_disable_comments_toggle' => 'Deaktiver kommentarer', 'app_disable_comments_desc' => 'Deaktiver kommentarer på tvers av alle sidene i applikasjonen.
      Eksisterende kommentarer vises ikke.', @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Papirkurven', 'recycle_bin_desc' => 'Her kan du gjenopprette ting du har kastet i papirkurven eller velge å slette dem permanent fra systemet. Denne listen er ikke filtrert i motsetning til lignende lister i systemet hvor tilgangskontroll overholdes.', 'recycle_bin_deleted_item' => 'Kastet element', + 'recycle_bin_deleted_parent' => 'Overordnet', 'recycle_bin_deleted_by' => 'Kastet av', 'recycle_bin_deleted_at' => 'Kastet den', 'recycle_bin_permanently_delete' => 'Slett permanent', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Elementer som skal gjenopprettes', 'recycle_bin_restore_confirm' => 'Denne handlingen vil hente opp elementet fra papirkurven, inkludert underliggende innhold, til sin opprinnelige sted. Om den opprinnelige plassen har blitt slettet i mellomtiden og nå befinner seg i papirkurven, vil også dette bli hentet opp igjen.', 'recycle_bin_restore_deleted_parent' => 'Det overordnede elementet var også kastet i papirkurven. Disse elementene vil forbli kastet inntil det overordnede også hentes opp igjen.', + 'recycle_bin_restore_parent' => 'Gjenopprett overodnet', 'recycle_bin_destroy_notification' => 'Slettet :count elementer fra papirkurven.', 'recycle_bin_restore_notification' => 'Gjenopprettet :count elementer fra papirkurven.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Rolledetaljer', 'role_name' => 'Rollenavn', 'role_desc' => 'Kort beskrivelse av rolle', + 'role_mfa_enforced' => 'Krever flerfaktorautentisering', 'role_external_auth_id' => 'Ekstern godkjennings-ID', 'role_system' => 'Systemtilganger', 'role_manage_users' => 'Behandle kontoer', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Behandle sidemaler', 'role_access_api' => 'Systemtilgang API', 'role_manage_settings' => 'Behandle applikasjonsinnstillinger', + 'role_export_content' => 'Export content', 'role_asset' => 'Eiendomstillatelser', 'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.', 'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Opprett nøkkel', 'users_api_tokens_expires' => 'Utløper', 'users_api_tokens_docs' => 'API-dokumentasjon', + 'users_mfa' => 'Flerfaktorautentisering', + 'users_mfa_desc' => 'Konfigurer flerfaktorautentisering som et ekstra lag med sikkerhet for din konto.', + 'users_mfa_x_methods' => ':count metode konfigurert|:count metoder konfigurert', + 'users_mfa_configure' => 'Konfigurer metoder', // API Tokens 'user_api_token_create' => 'Opprett API-nøkkel', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/nb/validation.php b/resources/lang/nb/validation.php index d06240c7c..684645729 100644 --- a/resources/lang/nb/validation.php +++ b/resources/lang/nb/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute kan kunne inneholde bokstaver, tall, bindestreker eller understreker.', 'alpha_num' => ':attribute kan kun inneholde bokstaver og tall.', 'array' => ':attribute må være en liste.', + 'backup_codes' => 'Den angitte koden er ikke gyldig, eller er allerede benyttet.', 'before' => ':attribute må være en dato før :date.', 'between' => [ 'numeric' => ':attribute må være mellom :min og :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute må være en tekststreng.', 'timezone' => ':attribute må være en tidssone.', + 'totp' => 'Den angitte koden er ikke gyldig eller har utløpt.', 'unique' => ':attribute har allerede blitt tatt.', 'url' => ':attribute format er ugyldig.', 'uploaded' => 'kunne ikke lastes opp, tjeneren støtter ikke filer av denne størrelsen.', diff --git a/resources/lang/nl/activities.php b/resources/lang/nl/activities.php index fcbad2400..f45ea074a 100644 --- a/resources/lang/nl/activities.php +++ b/resources/lang/nl/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" is toegevoegd aan je favorieten', 'favourite_remove_notification' => '":name" is verwijderd uit je favorieten', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'reageerde op', 'permissions_update' => 'wijzigde permissies', diff --git a/resources/lang/nl/auth.php b/resources/lang/nl/auth.php index fcff3fd48..f57b2ecc7 100644 --- a/resources/lang/nl/auth.php +++ b/resources/lang/nl/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Welkom bij :appName!', 'user_invite_page_text' => 'Om je account af te ronden en toegang te krijgen moet je een wachtwoord instellen dat gebruikt wordt om in te loggen op :appName bij toekomstige bezoeken.', 'user_invite_page_confirm_button' => 'Bevestig wachtwoord', - 'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :appName!' + 'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/nl/common.php b/resources/lang/nl/common.php index 67d0876c7..c53df6f2c 100644 --- a/resources/lang/nl/common.php +++ b/resources/lang/nl/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Resetten', 'remove' => 'Verwijderen', 'add' => 'Toevoegen', + 'configure' => 'Configure', 'fullscreen' => 'Volledig scherm', 'favourite' => 'Favoriet', 'unfavourite' => 'Verwijderen uit favoriet', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Geen activiteit om weer te geven', 'no_items' => 'Geen items beschikbaar', 'back_to_top' => 'Terug naar boven', + 'skip_to_main_content' => 'Direct naar de hoofdinhoud', 'toggle_details' => 'Details weergeven', 'toggle_thumbnails' => 'Thumbnails weergeven', 'details' => 'Details', diff --git a/resources/lang/nl/entities.php b/resources/lang/nl/entities.php index eecdfd589..56ef9a07a 100644 --- a/resources/lang/nl/entities.php +++ b/resources/lang/nl/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Ingesloten webbestand', 'export_pdf' => 'PDF bestand', 'export_text' => 'Normaal tekstbestand', + 'export_md' => 'Markdown bestand', // Permissions and restrictions 'permissions' => 'Permissies', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Boekenplank permissies', 'shelves_permissions_updated' => 'Boekenplank permissies opgeslagen', 'shelves_permissions_active' => 'Boekenplank permissies actief', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopieer permissies naar boeken', 'shelves_copy_permissions' => 'Kopieer permissies', 'shelves_copy_permissions_explain' => 'Met deze actie worden de permissies van deze boekenplank gekopieërd naar alle boeken op de plank. Voordat deze actie wordt uitgevoerd, zorg dat de wijzigingen in de permissies van deze boekenplank zijn opgeslagen.', diff --git a/resources/lang/nl/settings.php b/resources/lang/nl/settings.php index 54cee27b1..a9f317e22 100644 --- a/resources/lang/nl/settings.php +++ b/resources/lang/nl/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Prullenbak', 'recycle_bin_desc' => 'Hier kunt u items herstellen die zijn verwijderd of kiezen om ze permanent te verwijderen uit het systeem. Deze lijst is niet gefilterd, in tegenstelling tot vergelijkbare activiteitenlijsten in het systeem waar rechtenfilters worden toegepast.', 'recycle_bin_deleted_item' => 'Verwijderde Item', + 'recycle_bin_deleted_parent' => 'Bovenliggende', 'recycle_bin_deleted_by' => 'Verwijderd door', 'recycle_bin_deleted_at' => 'Verwijdert op', 'recycle_bin_permanently_delete' => 'Permanent verwijderen', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items te herstellen', 'recycle_bin_restore_confirm' => 'Deze actie herstelt het verwijderde item, inclusief alle onderliggende elementen, op hun oorspronkelijke locatie. Als de oorspronkelijke locatie sindsdien is verwijderd en zich nu in de prullenbak bevindt, zal ook het bovenliggende item moeten worden hersteld.', 'recycle_bin_restore_deleted_parent' => 'De bovenliggende map van dit item is ook verwijderd. Deze zal worden verwijderd totdat het bovenliggende item ook is hersteld.', + 'recycle_bin_restore_parent' => 'Herstel bovenliggende', 'recycle_bin_destroy_notification' => 'Verwijderde totaal :count items uit de prullenbak.', 'recycle_bin_restore_notification' => 'Herstelde totaal :count items uit de prullenbak.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Rol Details', 'role_name' => 'Rolnaam', 'role_desc' => 'Korte beschrijving van de rol', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Externe authenticatie ID\'s', 'role_system' => 'Systeem Permissies', 'role_manage_users' => 'Gebruikers beheren', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Paginasjablonen beheren', 'role_access_api' => 'Ga naar systeem API', 'role_manage_settings' => 'Beheer app instellingen', + 'role_export_content' => 'Export content', 'role_asset' => 'Asset Permissies', 'roles_system_warning' => 'Wees ervan bewust dat toegang tot een van de bovengenoemde drie machtigingen een gebruiker in staat kan stellen zijn eigen privileges of de privileges van anderen in het systeem te wijzigen. Wijs alleen rollen toe met deze machtigingen aan vertrouwde gebruikers.', 'role_asset_desc' => 'Deze permissies bepalen de standaardtoegangsrechten. Permissies op boeken, hoofdstukken en pagina\'s overschrijven deze instelling.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Token aanmaken', 'users_api_tokens_expires' => 'Verloopt', 'users_api_tokens_docs' => 'API Documentatie', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'API-token aanmaken', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/nl/validation.php b/resources/lang/nl/validation.php index f0e99ad91..c572ce85b 100644 --- a/resources/lang/nl/validation.php +++ b/resources/lang/nl/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute mag alleen letters, cijfers, streepjes en liggende streepjes bevatten.', 'alpha_num' => ':attribute mag alleen letters en nummers bevatten.', 'array' => ':attribute moet een reeks zijn.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute moet een datum zijn voor :date.', 'between' => [ 'numeric' => ':attribute moet tussen de :min en :max zijn.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute moet tekst zijn.', 'timezone' => ':attribute moet een geldige zone zijn.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute is al in gebruik.', 'url' => ':attribute formaat is ongeldig.', 'uploaded' => 'Het bestand kon niet worden geüpload. De server accepteert mogelijk geen bestanden van deze grootte.', diff --git a/resources/lang/pl/activities.php b/resources/lang/pl/activities.php index 13e35e206..9c984d46e 100644 --- a/resources/lang/pl/activities.php +++ b/resources/lang/pl/activities.php @@ -44,8 +44,12 @@ return [ 'bookshelf_delete_notification' => 'Półka usunięta pomyślnie', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" został dodany do Twoich ulubionych', + 'favourite_remove_notification' => '":name" został usunięty z ulubionych', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other 'commented_on' => 'skomentował', diff --git a/resources/lang/pl/auth.php b/resources/lang/pl/auth.php index 01d74b99c..d2439f2d3 100644 --- a/resources/lang/pl/auth.php +++ b/resources/lang/pl/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Witaj w :appName!', 'user_invite_page_text' => 'Aby zakończyć tworzenie konta musisz ustawić hasło, które będzie używane do logowania do :appName w przyszłości.', 'user_invite_page_confirm_button' => 'Potwierdź hasło', - 'user_invite_success' => 'Hasło zostało ustawione, teraz masz dostęp do :appName!' + 'user_invite_success' => 'Hasło zostało ustawione, teraz masz dostęp do :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/pl/common.php b/resources/lang/pl/common.php index 716ee7ae5..42a0a312b 100644 --- a/resources/lang/pl/common.php +++ b/resources/lang/pl/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Resetuj', 'remove' => 'Usuń', 'add' => 'Dodaj', + 'configure' => 'Configure', 'fullscreen' => 'Pełny ekran', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'next' => 'Dalej', + 'previous' => 'Wstecz', // Sort Options 'sort_options' => 'Opcje sortowania', @@ -51,7 +52,7 @@ return [ 'sort_ascending' => 'Sortuj rosnąco', 'sort_descending' => 'Sortuj malejąco', 'sort_name' => 'Nazwa', - 'sort_default' => 'Default', + 'sort_default' => 'Domyślne', 'sort_created_at' => 'Data utworzenia', 'sort_updated_at' => 'Data aktualizacji', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Brak aktywności do wyświetlenia', 'no_items' => 'Brak elementów do wyświetlenia', 'back_to_top' => 'Powrót na górę', + 'skip_to_main_content' => 'Przejdź do treści głównej', 'toggle_details' => 'Włącz/wyłącz szczegóły', 'toggle_thumbnails' => 'Włącz/wyłącz miniatury', 'details' => 'Szczegóły', diff --git a/resources/lang/pl/entities.php b/resources/lang/pl/entities.php index 9d172a873..138062109 100644 --- a/resources/lang/pl/entities.php +++ b/resources/lang/pl/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Plik HTML', 'export_pdf' => 'Plik PDF', 'export_text' => 'Plik tekstowy', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Uprawnienia', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Uprawnienia półki', 'shelves_permissions_updated' => 'Uprawnienia półki zostały zaktualizowane', 'shelves_permissions_active' => 'Uprawnienia półki są aktywne', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Skopiuj uprawnienia do książek', 'shelves_copy_permissions' => 'Skopiuj uprawnienia', 'shelves_copy_permissions_explain' => 'To spowoduje zastosowanie obecnych ustawień uprawnień dla tej półki do wszystkich książek w niej zawartych. Przed aktywacją upewnij się, że wszelkie zmiany w uprawnieniach do tej półki zostały zapisane.', diff --git a/resources/lang/pl/errors.php b/resources/lang/pl/errors.php index 236c787a7..488b753c6 100644 --- a/resources/lang/pl/errors.php +++ b/resources/lang/pl/errors.php @@ -83,9 +83,9 @@ return [ '404_page_not_found' => 'Strona nie została znaleziona', 'sorry_page_not_found' => 'Przepraszamy, ale strona której szukasz nie została znaleziona.', 'sorry_page_not_found_permission_warning' => 'Jeśli spodziewałeś się, że ta strona istnieje, prawdopodobnie nie masz uprawnień do jej wyświetlenia.', - 'image_not_found' => 'Image Not Found', - 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', - 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', + 'image_not_found' => 'Nie znaleziono obrazu', + 'image_not_found_subtitle' => 'Przepraszamy, ale obraz którego szukasz nie został znaleziony.', + 'image_not_found_details' => 'Jeśli spodziewałeś się, że ten obraz istnieje, mógł on zostać usunięty.', 'return_home' => 'Powrót do strony głównej', 'error_occurred' => 'Wystąpił błąd', 'app_down' => ':appName jest aktualnie wyłączona', diff --git a/resources/lang/pl/settings.php b/resources/lang/pl/settings.php index 98130dafa..686435927 100644 --- a/resources/lang/pl/settings.php +++ b/resources/lang/pl/settings.php @@ -16,7 +16,7 @@ return [ 'app_features_security' => 'Funkcje i bezpieczeństwo', 'app_name' => 'Nazwa aplikacji', 'app_name_desc' => 'Ta nazwa jest wyświetlana w nagłówku i e-mailach.', - 'app_name_header' => 'Pokazać nazwę aplikacji w nagłówku?', + 'app_name_header' => 'Pokaż nazwę aplikacji w nagłówku', 'app_public_access' => 'Dostęp publiczny', 'app_public_access_desc' => 'Włączenie tej opcji umożliwi niezalogowanym odwiedzającym dostęp do treści w Twojej instancji BookStack.', 'app_public_access_desc_guest' => 'Dostęp dla niezalogowanych odwiedzających jest dostępny poprzez użytkownika "Guest".', @@ -59,7 +59,7 @@ return [ 'reg_settings' => 'Ustawienia rejestracji', 'reg_enable' => 'Włącz rejestrację', 'reg_enable_toggle' => 'Włącz rejestrację', - 'reg_enable_desc' => 'Kiedy rejestracja jest włączona użytkownicy mogą się rejestrować. Po rejestracji otrzymują jedną domyślną rolę użytkownika.', + 'reg_enable_desc' => 'Po włączeniu rejestracji użytkownicy ci będą mogli się samodzielnie zarejestrować i otrzymają domyślną rolę.', 'reg_default_role' => 'Domyślna rola użytkownika po rejestracji', 'reg_enable_external_warning' => 'Powyższa opcja jest ignorowana, gdy zewnętrzne uwierzytelnianie LDAP lub SAML jest aktywne. Konta użytkowników dla nieistniejących użytkowników zostaną automatycznie utworzone, jeśli uwierzytelnianie za pomocą systemu zewnętrznego zakończy się sukcesem.', 'reg_email_confirmation' => 'Potwierdzenie adresu email', @@ -90,22 +90,24 @@ return [ // Recycle Bin 'recycle_bin' => 'Kosz', - 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', + 'recycle_bin_desc' => 'Tutaj możesz przywrócić elementy, które zostały usunięte lub usunąć je z systemu. Ta lista jest niefiltrowana w odróżnieniu od podobnych list aktywności w systemie, w którym stosowane są filtry uprawnień.', 'recycle_bin_deleted_item' => 'Usunięta pozycja', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Usunięty przez', 'recycle_bin_deleted_at' => 'Czas usunięcia', 'recycle_bin_permanently_delete' => 'Usuń trwale', 'recycle_bin_restore' => 'Przywróć', 'recycle_bin_contents_empty' => 'Kosz jest pusty', 'recycle_bin_empty' => 'Opróżnij kosz', - 'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?', + 'recycle_bin_empty_confirm' => 'To na stałe zniszczy wszystkie przedmioty w koszu, w tym zawartość w każdym elemencie. Czy na pewno chcesz opróżnić kosz?', 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?', - 'recycle_bin_destroy_list' => 'Items to be Destroyed', + 'recycle_bin_destroy_list' => 'Elementy do usunięcia', 'recycle_bin_restore_list' => 'Elementy do przywrócenia', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', - 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', - 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', + 'recycle_bin_restore_parent' => 'Restore Parent', + 'recycle_bin_destroy_notification' => 'Usunięto :count przedmiotów z kosza.', + 'recycle_bin_restore_notification' => 'Przywrócono :count przedmiotów z kosza.', // Audit Log 'audit' => 'Dziennik audytu', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Szczegóły roli', 'role_name' => 'Nazwa roli', 'role_desc' => 'Krótki opis roli', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Zewnętrzne identyfikatory uwierzytelniania', 'role_system' => 'Uprawnienia systemowe', 'role_manage_users' => 'Zarządzanie użytkownikami', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Zarządzaj szablonami stron', 'role_access_api' => 'Dostęp do systemowego API', 'role_manage_settings' => 'Zarządzanie ustawieniami aplikacji', + 'role_export_content' => 'Export content', 'role_asset' => 'Zarządzanie zasobami', 'roles_system_warning' => 'Pamiętaj, że dostęp do trzech powyższych uprawnień może pozwolić użytkownikowi na zmianę własnych uprawnień lub uprawnień innych osób w systemie. Przypisz tylko role z tymi uprawnieniami do zaufanych użytkowników.', 'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia książek, rozdziałów i stron nadpisują te ustawienia.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Utwórz token', 'users_api_tokens_expires' => 'Wygasa', 'users_api_tokens_docs' => 'Dokumentacja API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Utwórz klucz API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/pl/validation.php b/resources/lang/pl/validation.php index 76ff49a6a..b1f28b8c1 100644 --- a/resources/lang/pl/validation.php +++ b/resources/lang/pl/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute może zawierać wyłącznie litery, cyfry i myślniki.', 'alpha_num' => ':attribute może zawierać wyłącznie litery i cyfry.', 'array' => ':attribute musi być tablicą.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute musi być datą poprzedzającą :date.', 'between' => [ 'numeric' => ':attribute musi zawierać się w przedziale od :min do :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute musi być ciągiem znaków.', 'timezone' => ':attribute musi być prawidłową strefą czasową.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute zostało już zajęte.', 'url' => 'Format :attribute jest nieprawidłowy.', 'uploaded' => 'Plik nie może zostać wysłany. Serwer nie akceptuje plików o takim rozmiarze.', diff --git a/resources/lang/pt/activities.php b/resources/lang/pt/activities.php index 20194394f..8bf2ff9fc 100644 --- a/resources/lang/pt/activities.php +++ b/resources/lang/pt/activities.php @@ -44,8 +44,12 @@ return [ 'bookshelf_delete_notification' => 'Estante eliminada com sucesso', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" foi adicionado aos seus favoritos', + 'favourite_remove_notification' => '":name" foi removido dos seus favoritos', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other 'commented_on' => 'comentado a', diff --git a/resources/lang/pt/auth.php b/resources/lang/pt/auth.php index 7d520a5f7..de3d35e97 100644 --- a/resources/lang/pt/auth.php +++ b/resources/lang/pt/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!', 'user_invite_page_text' => 'Para finalizar a sua conta e obter acesso, precisa de definir uma senha que será utilizada para efetuar login em :appName em visitas futuras.', 'user_invite_page_confirm_button' => 'Confirmar Palavra-Passe', - 'user_invite_success' => 'Palavra-passe definida, tem agora acesso a :appName!' + 'user_invite_success' => 'Palavra-passe definida, tem agora acesso a :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/pt/common.php b/resources/lang/pt/common.php index 7dd2c9c63..19a5dc24a 100644 --- a/resources/lang/pt/common.php +++ b/resources/lang/pt/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Redefinir', 'remove' => 'Remover', 'add' => 'Adicionar', + 'configure' => 'Configure', 'fullscreen' => 'Ecrã completo', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Favorito', + 'unfavourite' => 'Retirar Favorito', + 'next' => 'Próximo', + 'previous' => 'Anterior', // Sort Options 'sort_options' => 'Opções de Ordenação', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nenhuma atividade a mostrar', 'no_items' => 'Nenhum item disponível', 'back_to_top' => 'Voltar ao topo', + 'skip_to_main_content' => 'Avançar para o conteúdo principal', 'toggle_details' => 'Alternar Detalhes', 'toggle_thumbnails' => 'Alternar Miniaturas', 'details' => 'Detalhes', diff --git a/resources/lang/pt/entities.php b/resources/lang/pt/entities.php index 0b258f144..1cd5c277f 100644 --- a/resources/lang/pt/entities.php +++ b/resources/lang/pt/entities.php @@ -27,8 +27,8 @@ return [ 'images' => 'Imagens', 'my_recent_drafts' => 'Os Meus Rascunhos Recentes', 'my_recently_viewed' => 'Visualizados Recentemente Por Mim', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => 'Os Meus Favoritos Mais Visualizados', + 'my_favourites' => 'Os Meus Favoritos', 'no_pages_viewed' => 'Você não viu nenhuma página', 'no_pages_recently_created' => 'Nenhuma página foi recentemente criada', 'no_pages_recently_updated' => 'Nenhuma página foi recentemente atualizada', @@ -36,6 +36,7 @@ return [ 'export_html' => 'Arquivo Web contido', 'export_pdf' => 'Arquivo PDF', 'export_text' => 'Arquivo Texto', + 'export_md' => 'Ficheiro Markdown', // Permissions and restrictions 'permissions' => 'Permissões', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permissões da Estante', 'shelves_permissions_updated' => 'Permissões da Estante de Livros Atualizada', 'shelves_permissions_active' => 'Permissões da Estante de Livros Ativas', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros', 'shelves_copy_permissions' => 'Copiar Permissões', 'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta estante a todos os livros nela contidos. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta estante foram guardadas.', diff --git a/resources/lang/pt/settings.php b/resources/lang/pt/settings.php index c60b4dd1b..39f28424e 100644 --- a/resources/lang/pt/settings.php +++ b/resources/lang/pt/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Reciclagem', 'recycle_bin_desc' => 'Aqui pode restaurar itens que foram eliminados ou eliminá-los permanentemente do sistema. Esta lista não é filtrada diferentemente de listas de atividades parecidas no sistema onde filtros de permissão são aplicados.', 'recycle_bin_deleted_item' => 'Item eliminado', + 'recycle_bin_deleted_parent' => 'Parente', 'recycle_bin_deleted_by' => 'Eliminado por', 'recycle_bin_deleted_at' => 'Data de Eliminação', 'recycle_bin_permanently_delete' => 'Eliminar permanentemente', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Itens a serem Restaurados', 'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para o seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na reciclagem, o item pai também precisará de ser restaurado.', 'recycle_bin_restore_deleted_parent' => 'O parente deste item foi também eliminado. Estes permanecerão eliminados até que o parente seja também restaurado.', + 'recycle_bin_restore_parent' => 'Restaurar Parente', 'recycle_bin_destroy_notification' => 'Eliminados no total :count itens da lixeira.', 'recycle_bin_restore_notification' => 'Restaurados no total :count itens da reciclagem.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detalhes do Cargo', 'role_name' => 'Nome do Cargo', 'role_desc' => 'Breve Descrição do Cargo', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'IDs de Autenticação Externa', 'role_system' => 'Permissões do Sistema', 'role_manage_users' => 'Gerir utilizadores', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Gerir modelos de página', 'role_access_api' => 'Aceder à API do sistema', 'role_manage_settings' => 'Gerir as configurações da aplicação', + 'role_export_content' => 'Export content', 'role_asset' => 'Permissões de Ativos', 'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.', 'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Criar Token', 'users_api_tokens_expires' => 'Expira', 'users_api_tokens_docs' => 'Documentação da API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Criar Token de API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/pt/validation.php b/resources/lang/pt/validation.php index b36ed0dab..30033fce8 100644 --- a/resources/lang/pt/validation.php +++ b/resources/lang/pt/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e sublinhado.', 'alpha_num' => 'O campo :attribute deve conter apenas letras e números.', 'array' => 'O campo :attribute deve ser uma lista(array).', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'O campo :attribute deve ser uma data anterior à data :date.', 'between' => [ 'numeric' => 'O campo :attribute deve estar entre :min e :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'O campo :attribute deve ser uma string.', 'timezone' => 'O campo :attribute deve conter uma timezone válida.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'Já existe um campo/dado de nome :attribute.', 'url' => 'O formato da URL :attribute é inválido.', 'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.', diff --git a/resources/lang/pt_BR/activities.php b/resources/lang/pt_BR/activities.php index ad5a34398..487a6fce6 100644 --- a/resources/lang/pt_BR/activities.php +++ b/resources/lang/pt_BR/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'comentou em', 'permissions_update' => 'atualizou permissões', diff --git a/resources/lang/pt_BR/auth.php b/resources/lang/pt_BR/auth.php index b2c3072c4..a5f0c18bc 100644 --- a/resources/lang/pt_BR/auth.php +++ b/resources/lang/pt_BR/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!', 'user_invite_page_text' => 'Para finalizar sua conta e obter acesso, você precisa definir uma senha que será usada para efetuar login em :appName em futuras visitas.', 'user_invite_page_confirm_button' => 'Confirmar Senha', - 'user_invite_success' => 'Senha definida, você agora tem acesso a :appName!' + 'user_invite_success' => 'Senha definida, você agora tem acesso a :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/pt_BR/common.php b/resources/lang/pt_BR/common.php index 1757d955a..1435a380d 100644 --- a/resources/lang/pt_BR/common.php +++ b/resources/lang/pt_BR/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Redefinir', 'remove' => 'Remover', 'add' => 'Adicionar', + 'configure' => 'Configure', 'fullscreen' => 'Tela cheia', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Favoritos', + 'unfavourite' => 'Remover dos Favoritos', + 'next' => 'Seguinte', + 'previous' => 'Anterior', // Sort Options 'sort_options' => 'Opções de Ordenação', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Nenhuma atividade a mostrar', 'no_items' => 'Nenhum item disponível', 'back_to_top' => 'Voltar ao topo', + 'skip_to_main_content' => 'Ir para o conteúdo principal', 'toggle_details' => 'Alternar Detalhes', 'toggle_thumbnails' => 'Alternar Miniaturas', 'details' => 'Detalhes', diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php index a920bfd20..ad58879b5 100644 --- a/resources/lang/pt_BR/entities.php +++ b/resources/lang/pt_BR/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Arquivo Web Contained', 'export_pdf' => 'Arquivo PDF', 'export_text' => 'Arquivo Texto', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Permissões', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Permissões da Prateleira', 'shelves_permissions_updated' => 'Permissões da Prateleira Atualizadas', 'shelves_permissions_active' => 'Permissões da Prateleira Ativas', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros', 'shelves_copy_permissions' => 'Copiar Permissões', 'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta prateleira a todos os livros contidos nela. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta prateleira tenham sido salvas.', diff --git a/resources/lang/pt_BR/settings.php b/resources/lang/pt_BR/settings.php index f06ffa835..bb1bb2f31 100644 --- a/resources/lang/pt_BR/settings.php +++ b/resources/lang/pt_BR/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Lixeira', 'recycle_bin_desc' => 'Aqui você pode restaurar itens que foram excluídos ou escolher removê-los permanentemente do sistema. Esta lista não é filtrada diferentemente de listas de atividades similares no sistema onde filtros de permissão são aplicados.', 'recycle_bin_deleted_item' => 'Item excluído', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Excluído por', 'recycle_bin_deleted_at' => 'Momento de Exclusão', 'recycle_bin_permanently_delete' => 'Excluir permanentemente', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Itens a serem restaurados', 'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na lixeira, o item pai também precisará ser restaurado.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detalhes do Cargo', 'role_name' => 'Nome do Cargo', 'role_desc' => 'Breve Descrição do Cargo', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'IDs de Autenticação Externa', 'role_system' => 'Permissões do Sistema', 'role_manage_users' => 'Gerenciar usuários', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Gerenciar modelos de página', 'role_access_api' => 'Acessar API do sistema', 'role_manage_settings' => 'Gerenciar configurações da aplicação', + 'role_export_content' => 'Export content', 'role_asset' => 'Permissões de Ativos', 'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um usuário altere seus próprios privilégios ou privilégios de outros usuários no sistema. Apenas atribua cargos com essas permissões para usuários confiáveis.', 'role_asset_desc' => 'Essas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por essas permissões.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Criar Token', 'users_api_tokens_expires' => 'Expira', 'users_api_tokens_docs' => 'Documentação da API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Criar Token de API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/pt_BR/validation.php b/resources/lang/pt_BR/validation.php index ea3779c78..4bf85d7cf 100644 --- a/resources/lang/pt_BR/validation.php +++ b/resources/lang/pt_BR/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e underlines.', 'alpha_num' => 'O campo :attribute deve conter apenas letras e números.', 'array' => 'O campo :attribute deve ser uma array.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'O campo :attribute deve ser uma data anterior à data :date.', 'between' => [ 'numeric' => 'O campo :attribute deve estar entre :min e :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'O campo :attribute deve ser uma string.', 'timezone' => 'O campo :attribute deve conter uma timezone válida.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'Já existe um campo/dado de nome :attribute.', 'url' => 'O formato da URL :attribute é inválido.', 'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.', diff --git a/resources/lang/ru/activities.php b/resources/lang/ru/activities.php index 4f9fb07e9..5a9eb8170 100644 --- a/resources/lang/ru/activities.php +++ b/resources/lang/ru/activities.php @@ -45,7 +45,11 @@ return [ // Favourites 'favourite_add_notification' => '":name" добавлено в избранное', - 'favourite_remove_notification' => ':name" удалено из избранного', + 'favourite_remove_notification' => '":name" удалено из избранного', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other 'commented_on' => 'прокомментировал', diff --git a/resources/lang/ru/auth.php b/resources/lang/ru/auth.php index 1f0ec6b80..1c9c9309d 100644 --- a/resources/lang/ru/auth.php +++ b/resources/lang/ru/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Добро пожаловать в :appName!', 'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.', 'user_invite_page_confirm_button' => 'Подтвердите пароль', - 'user_invite_success' => 'Пароль установлен, теперь у вас есть доступ к :appName!' + 'user_invite_success' => 'Пароль установлен, теперь у вас есть доступ к :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/ru/common.php b/resources/lang/ru/common.php index fce83880e..6e2a31931 100644 --- a/resources/lang/ru/common.php +++ b/resources/lang/ru/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Сбросить', 'remove' => 'Удалить', 'add' => 'Добавить', + 'configure' => 'Configure', 'fullscreen' => 'На весь экран', 'favourite' => 'Избранное', 'unfavourite' => 'Убрать из избранного', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Нет действий для просмотра', 'no_items' => 'Нет доступных элементов', 'back_to_top' => 'Наверх', + 'skip_to_main_content' => 'Перейти к основному контенту', 'toggle_details' => 'Подробности', 'toggle_thumbnails' => 'Миниатюры', 'details' => 'Детали', diff --git a/resources/lang/ru/entities.php b/resources/lang/ru/entities.php index 6d666b504..42b06931c 100644 --- a/resources/lang/ru/entities.php +++ b/resources/lang/ru/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Веб файл', 'export_pdf' => 'PDF файл', 'export_text' => 'Текстовый файл', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Разрешения', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Доступы к книжной полке', 'shelves_permissions_updated' => 'Доступы к книжной полке обновлены', 'shelves_permissions_active' => 'Действующие разрешения книжной полки', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам', 'shelves_copy_permissions' => 'Копировать доступы', 'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.', diff --git a/resources/lang/ru/settings.php b/resources/lang/ru/settings.php index 676fc54b2..af37b7b67 100755 --- a/resources/lang/ru/settings.php +++ b/resources/lang/ru/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Корзина', 'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.', 'recycle_bin_deleted_item' => 'Удаленный элемент', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Удалён', 'recycle_bin_deleted_at' => 'Время удаления', 'recycle_bin_permanently_delete' => 'Удалить навсегда', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Элементы для восстановления', 'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.', 'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.', 'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Детали роли', 'role_name' => 'Название роли', 'role_desc' => 'Краткое описание роли', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Внешние ID авторизации', 'role_system' => 'Системные разрешения', 'role_manage_users' => 'Управление пользователями', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Управление шаблонами страниц', 'role_access_api' => 'Доступ к системному API', 'role_manage_settings' => 'Управление настройками приложения', + 'role_export_content' => 'Export content', 'role_asset' => 'Права доступа к материалам', 'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.', 'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Создать токен', 'users_api_tokens_expires' => 'Истекает', 'users_api_tokens_docs' => 'Документация', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Создать токен', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/ru/validation.php b/resources/lang/ru/validation.php index 8c583f7e7..5b3f8f766 100644 --- a/resources/lang/ru/validation.php +++ b/resources/lang/ru/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute может содержать только буквы, цифры и тире.', 'alpha_num' => ':attribute должен содержать только буквы и цифры.', 'array' => ':attribute должен быть массивом.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute дата должна быть до :date.', 'between' => [ 'numeric' => ':attribute должен быть между :min и :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute должен быть строкой.', 'timezone' => ':attribute должен быть корректным часовым поясом.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute уже есть.', 'url' => 'Формат :attribute некорректен.', 'uploaded' => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.', diff --git a/resources/lang/sk/activities.php b/resources/lang/sk/activities.php index 15897e791..91b9e1880 100644 --- a/resources/lang/sk/activities.php +++ b/resources/lang/sk/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'komentoval(a)', 'permissions_update' => 'aktualizované oprávnenia', diff --git a/resources/lang/sk/auth.php b/resources/lang/sk/auth.php index 0d96811a3..92cf4f648 100644 --- a/resources/lang/sk/auth.php +++ b/resources/lang/sk/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Vitajte v :appName!', 'user_invite_page_text' => 'Ak chcete dokončiť svoj účet a získať prístup, musíte nastaviť heslo, ktoré sa použije na prihlásenie do aplikácie :appName pri budúcich návštevách.', 'user_invite_page_confirm_button' => 'Potvrdiť heslo', - 'user_invite_success' => 'Heslo bolo nastavené, teraz máte prístup k :appName!' + 'user_invite_success' => 'Heslo bolo nastavené, teraz máte prístup k :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/sk/common.php b/resources/lang/sk/common.php index 7a2051688..aab959305 100644 --- a/resources/lang/sk/common.php +++ b/resources/lang/sk/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Resetovať', 'remove' => 'Odstrániť', 'add' => 'Pridať', + 'configure' => 'Configure', 'fullscreen' => 'Celá obrazovka', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Žiadna aktivita na zobrazenie', 'no_items' => 'Žiadne položky nie sú dostupné', 'back_to_top' => 'Späť nahor', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Prepnúť detaily', 'toggle_thumbnails' => 'Prepnúť náhľady', 'details' => 'Podrobnosti', diff --git a/resources/lang/sk/entities.php b/resources/lang/sk/entities.php index 7550255a9..1e97e8c03 100644 --- a/resources/lang/sk/entities.php +++ b/resources/lang/sk/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Obsahovaný webový súbor', 'export_pdf' => 'PDF súbor', 'export_text' => 'Súbor s čistým textom', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Oprávnenia', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Oprávnenia knižnice', 'shelves_permissions_updated' => 'Oprávnenia knižnice aktualizované', 'shelves_permissions_active' => 'Oprávnenia knižnice aktívne', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Copy Permissions to Books', 'shelves_copy_permissions' => 'Copy Permissions', 'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.', diff --git a/resources/lang/sk/settings.php b/resources/lang/sk/settings.php index eeb237981..b2d3421b0 100644 --- a/resources/lang/sk/settings.php +++ b/resources/lang/sk/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Recycle Bin', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Deleted Item', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Deleted By', 'recycle_bin_deleted_at' => 'Deletion Time', 'recycle_bin_permanently_delete' => 'Permanently Delete', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Detaily roly', 'role_name' => 'Názov roly', 'role_desc' => 'Krátky popis roly', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'Systémové oprávnenia', 'role_manage_users' => 'Spravovať používateľov', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Manage page templates', 'role_access_api' => 'Access system API', 'role_manage_settings' => 'Spravovať nastavenia aplikácie', + 'role_export_content' => 'Export content', 'role_asset' => 'Oprávnenia majetku', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Create Token', 'users_api_tokens_expires' => 'Expires', 'users_api_tokens_docs' => 'API Documentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/sk/validation.php b/resources/lang/sk/validation.php index 545313415..1f1a8841f 100644 --- a/resources/lang/sk/validation.php +++ b/resources/lang/sk/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute môže obsahovať iba písmená, čísla a pomlčky.', 'alpha_num' => ':attribute môže obsahovať iba písmená a čísla.', 'array' => ':attribute musí byť pole.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute musí byť dátum pred :date.', 'between' => [ 'numeric' => ':attribute musí byť medzi :min a :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute musí byť reťazec.', 'timezone' => ':attribute musí byť plantá časová zóna.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute je už použité.', 'url' => ':attribute formát je neplatný.', 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', diff --git a/resources/lang/sl/activities.php b/resources/lang/sl/activities.php index 91ec575e8..e1926c8ba 100644 --- a/resources/lang/sl/activities.php +++ b/resources/lang/sl/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'komentar na', 'permissions_update' => 'pravice so posodobljene', diff --git a/resources/lang/sl/auth.php b/resources/lang/sl/auth.php index df6fb4227..b6c41666a 100644 --- a/resources/lang/sl/auth.php +++ b/resources/lang/sl/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Dobrodošli na :appName!', 'user_invite_page_text' => 'Za zaključiti in pridobiti dostop si morate nastaviti geslo, ki bo uporabljeno za prijavo v :appName.', 'user_invite_page_confirm_button' => 'Potrdi geslo', - 'user_invite_success' => 'Geslo nastavljeno, sedaj imaš dostop do :appName!' + 'user_invite_success' => 'Geslo nastavljeno, sedaj imaš dostop do :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/sl/common.php b/resources/lang/sl/common.php index a34d91b33..9f478e75d 100644 --- a/resources/lang/sl/common.php +++ b/resources/lang/sl/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Ponastavi', 'remove' => 'Odstrani', 'add' => 'Dodaj', + 'configure' => 'Configure', 'fullscreen' => 'Celozaslonski način', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Ni aktivnosti za prikaz', 'no_items' => 'Na voljo ni nobenega elementa', 'back_to_top' => 'Nazaj na vrh', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Preklopi podrobnosti', 'toggle_thumbnails' => 'Preklopi sličice', 'details' => 'Podrobnosti', diff --git a/resources/lang/sl/entities.php b/resources/lang/sl/entities.php index 699b7c470..55f05e231 100644 --- a/resources/lang/sl/entities.php +++ b/resources/lang/sl/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Vsebuje spletno datoteko', 'export_pdf' => 'PDF datoteka (.pdf)', 'export_text' => 'Navadna besedilna datoteka', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Dovoljenja', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Dovoljenja knjižnih polic', 'shelves_permissions_updated' => 'Posodobljena dovoljenja knjižnih polic', 'shelves_permissions_active' => 'Aktivna dovoljenja knjižnih polic', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopiraj dovoljenja na knjige', 'shelves_copy_permissions' => 'Dovoljenja kopiranja', 'shelves_copy_permissions_explain' => 'To bo uveljavilo trenutne nastavitve dovoljenj na knjižni polici za vse knjige, ki jih vsebuje ta polica. Pred aktiviranjem zagotovite, da so shranjene vse spremembe dovoljenj te knjižne police.', diff --git a/resources/lang/sl/settings.php b/resources/lang/sl/settings.php index dc0382868..ff7458fb6 100644 --- a/resources/lang/sl/settings.php +++ b/resources/lang/sl/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Koš', 'recycle_bin_desc' => 'Tu lahko obnovite predmete, ki so bili izbrisani, ali pa jih trajno odstranite s sistema. Ta seznam je nefiltriran, za razliko od podobnih seznamov dejavnosti v sistemu, kjer se uporabljajo filtri dovoljenj.', 'recycle_bin_deleted_item' => 'Izbrisan element', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Izbrisal uporabnik', 'recycle_bin_deleted_at' => 'Čas izbrisa', 'recycle_bin_permanently_delete' => 'Trajno izbrišem?', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Predmeti, ki naj bodo obnovljeni', 'recycle_bin_restore_confirm' => 'S tem dejanjem boste izbrisani element, vključno z vsemi podrejenimi elementi, obnovili na prvotno mesto. Če je bilo prvotno mesto od takrat izbrisano in je zdaj v košu, bo treba obnoviti tudi nadrejeni element.', 'recycle_bin_restore_deleted_parent' => 'Nadrejeni element je bil prav tako izbrisan. Dokler se ne obnovi nadrejenega elementa, ni mogoče obnoviti njemu podrejenih elementov.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Izbrisano :count skupno število elementov iz koša.', 'recycle_bin_restore_notification' => 'Obnovljeno :count skupno število elementov iz koša.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Podrobnosti vloge', 'role_name' => 'Naziv vloge', 'role_desc' => 'Kratki opis vloge', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Zunanje dokazilo ID', 'role_system' => 'Sistemska dovoljenja', 'role_manage_users' => 'Upravljanje uporabnikov', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Uredi predloge', 'role_access_api' => 'API za dostop do sistema', 'role_manage_settings' => 'Nastavitve za upravljanje', + 'role_export_content' => 'Export content', 'role_asset' => 'Sistemska dovoljenja', 'roles_system_warning' => 'Zavedajte se, da lahko dostop do kateregakoli od zgornjih treh dovoljenj uporabniku omogoči, da spremeni lastne privilegije ali privilegije drugih v sistemu. Vloge s temi dovoljenji dodelite samo zaupanja vrednim uporabnikom.', 'role_asset_desc' => 'Ta dovoljenja nadzorujejo privzeti dostop do sredstev v sistemu. Dovoljenja za knjige, poglavja in strani bodo razveljavila ta dovoljenja.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Ustvari žeton', 'users_api_tokens_expires' => 'Poteče', 'users_api_tokens_docs' => 'API dokumentacija', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Ustvari žeton', @@ -248,6 +256,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/sl/validation.php b/resources/lang/sl/validation.php index 9b1a5ff46..5b08463ca 100644 --- a/resources/lang/sl/validation.php +++ b/resources/lang/sl/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute lahko vsebuje samo ?rke, ?tevilke in ?rtice.', 'alpha_num' => ':attribute lahko vsebuje samo črke in številke.', 'array' => ':attribute mora biti niz.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute mora biti datum pred :date.', 'between' => [ 'numeric' => ':attribute mora biti med :min in :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute mora biti niz.', 'timezone' => ':attribute mora biti veljavna cona.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute je že zaseden.', 'url' => ':attribute oblika ni veljavna.', 'uploaded' => 'Datoteke ni bilo mogoče naložiti. Strežnik morda ne sprejema datotek te velikosti.', diff --git a/resources/lang/sv/activities.php b/resources/lang/sv/activities.php index 30c157a66..a1315dcff 100644 --- a/resources/lang/sv/activities.php +++ b/resources/lang/sv/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" har lagts till i dina favoriter', 'favourite_remove_notification' => '":name" har tagits bort från dina favoriter', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'kommenterade', 'permissions_update' => 'uppdaterade behörigheter', diff --git a/resources/lang/sv/auth.php b/resources/lang/sv/auth.php index 866905535..1d1a81c74 100644 --- a/resources/lang/sv/auth.php +++ b/resources/lang/sv/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Välkommen till :appName!', 'user_invite_page_text' => 'För att slutföra ditt konto och få åtkomst måste du ange ett lösenord som kommer att användas för att logga in på :appName vid framtida besök.', 'user_invite_page_confirm_button' => 'Bekräfta lösenord', - 'user_invite_success' => 'Lösenord satt, du har nu tillgång till :appName!' + 'user_invite_success' => 'Lösenord satt, du har nu tillgång till :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/sv/common.php b/resources/lang/sv/common.php index 1482eeaab..177a8abef 100644 --- a/resources/lang/sv/common.php +++ b/resources/lang/sv/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Återställ', 'remove' => 'Radera', 'add' => 'Lägg till', + 'configure' => 'Configure', 'fullscreen' => 'Helskärm', 'favourite' => 'Favorit', 'unfavourite' => 'Ta bort favorit', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Ingen aktivitet att visa', 'no_items' => 'Inga tillgängliga föremål', 'back_to_top' => 'Tillbaka till toppen', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Växla detaljer', 'toggle_thumbnails' => 'Växla miniatyrer', 'details' => 'Information', diff --git a/resources/lang/sv/entities.php b/resources/lang/sv/entities.php index ce0dfddf2..301426583 100644 --- a/resources/lang/sv/entities.php +++ b/resources/lang/sv/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Webb-fil', 'export_pdf' => 'PDF-fil', 'export_text' => 'Textfil', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Rättigheter', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Bokhyllerättigheter', 'shelves_permissions_updated' => 'Bokhyllerättigheterna har ändrats', 'shelves_permissions_active' => 'Bokhyllerättigheterna är aktiva', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Kopiera rättigheter till böcker', 'shelves_copy_permissions' => 'Kopiera rättigheter', 'shelves_copy_permissions_explain' => 'Detta kommer kopiera hyllans rättigheter till alla böcker på den. Se till att du har sparat alla ändringar innan du går vidare.', diff --git a/resources/lang/sv/settings.php b/resources/lang/sv/settings.php index d5044efbd..18f4d49ed 100644 --- a/resources/lang/sv/settings.php +++ b/resources/lang/sv/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Papperskorgen', 'recycle_bin_desc' => 'Här kan du återställa objekt som har tagits bort eller välja att permanent ta bort dem från systemet. Denna lista är ofiltrerad till skillnad från liknande aktivitetslistor i systemet där behörighetsfilter tillämpas.', 'recycle_bin_deleted_item' => 'Raderat objekt', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Borttagen av', 'recycle_bin_deleted_at' => 'Tid för borttagning', 'recycle_bin_permanently_delete' => 'Radera permanent', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Objekt som ska återställas', 'recycle_bin_restore_confirm' => 'Denna åtgärd kommer att återställa det raderade objektet, inklusive alla underordnade element, till deras ursprungliga plats. Om den ursprungliga platsen har tagits bort sedan dess, och är nu i papperskorgen, kommer det överordnade objektet också att behöva återställas.', 'recycle_bin_restore_deleted_parent' => 'Föräldern till det här objektet har också tagits bort. Dessa kommer att förbli raderade tills den förälder är återställd.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Raderade :count totala objekt från papperskorgen.', 'recycle_bin_restore_notification' => 'Återställt :count totala objekt från papperskorgen.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Om rollen', 'role_name' => 'Rollens namn', 'role_desc' => 'Kort beskrivning av rollen', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Externa autentiserings-ID:n', 'role_system' => 'Systemrättigheter', 'role_manage_users' => 'Hanter användare', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Hantera mallar', 'role_access_api' => 'Åtkomst till systemets API', 'role_manage_settings' => 'Hantera appinställningar', + 'role_export_content' => 'Export content', 'role_asset' => 'Tillgång till innehåll', 'roles_system_warning' => 'Var medveten om att åtkomst till någon av ovanstående tre behörigheter kan tillåta en användare att ändra sina egna rättigheter eller andras rättigheter i systemet. Tilldela endast roller med dessa behörigheter till betrodda användare.', '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.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Skapa token', 'users_api_tokens_expires' => 'Förfaller', 'users_api_tokens_docs' => 'API-dokumentation', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Skapa API-nyckel', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/sv/validation.php b/resources/lang/sv/validation.php index da39796bc..0c9cc3164 100644 --- a/resources/lang/sv/validation.php +++ b/resources/lang/sv/validation.php @@ -15,6 +15,7 @@ return [ '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.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute måste vara före :date.', 'between' => [ 'numeric' => ':attribute måste vara mellan :min och :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute måste vara en sträng.', 'timezone' => ':attribute måste vara en giltig tidszon.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute är upptaget', 'url' => 'Formatet på :attribute är ogiltigt.', 'uploaded' => 'Filen kunde inte laddas upp. Servern kanske inte tillåter filer med denna storlek.', diff --git a/resources/lang/tr/activities.php b/resources/lang/tr/activities.php index 9bcd1891f..9a6e60cd4 100644 --- a/resources/lang/tr/activities.php +++ b/resources/lang/tr/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" favorilerinize eklendi', 'favourite_remove_notification' => '":name" favorilerinizden çıkarıldı', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'yorum yaptı', 'permissions_update' => 'güncellenmiş izinler', diff --git a/resources/lang/tr/auth.php b/resources/lang/tr/auth.php index 6a0e2c1b5..0ce90d4d0 100644 --- a/resources/lang/tr/auth.php +++ b/resources/lang/tr/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => ':appName uygulamasına hoş geldiniz!', 'user_invite_page_text' => 'Hesap kurulumunuzu tamamlamak ve gelecekteki :appName ziyaretlerinizde hesabınıza erişim sağlayabilmeniz için bir şifre belirlemeniz gerekiyor.', 'user_invite_page_confirm_button' => 'Şifreyi Onayla', - 'user_invite_success' => 'Şifreniz ayarlandı, artık :appName uygulamasına giriş yapabilirsiniz!' + 'user_invite_success' => 'Şifreniz ayarlandı, artık :appName uygulamasına giriş yapabilirsiniz!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/tr/common.php b/resources/lang/tr/common.php index 50ce70e61..1f19a62f4 100644 --- a/resources/lang/tr/common.php +++ b/resources/lang/tr/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Sıfırla', 'remove' => 'Kaldır', 'add' => 'Ekle', + 'configure' => 'Configure', 'fullscreen' => 'Tam Ekran', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Gösterilecek eylem bulunamadı', 'no_items' => 'Herhangi bir öge bulunamadı', 'back_to_top' => 'Başa dön', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Detayları Göster/Gizle', 'toggle_thumbnails' => 'Ön İzleme Görsellerini Göster/Gizle', 'details' => 'Detaylar', diff --git a/resources/lang/tr/entities.php b/resources/lang/tr/entities.php index e48277a4b..770918dd0 100644 --- a/resources/lang/tr/entities.php +++ b/resources/lang/tr/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Web Dosyası', 'export_pdf' => 'PDF Dosyası', 'export_text' => 'Düz Metin Dosyası', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'İzinler', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Kitaplık İzinleri', 'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi', 'shelves_permissions_active' => 'Kitaplık İzinleri Aktif', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'İzinleri Kitaplara Kopyala', 'shelves_copy_permissions' => 'İzinleri Kopyala', 'shelves_copy_permissions_explain' => 'Bu işlem sonucunda kitaplığınızın izinleri, içerdiği kitaplara da aynen uygulanır. Aktifleştirmeden önce bu kitaplığa ait izinleri kaydettiğinizden emin olun.', diff --git a/resources/lang/tr/settings.php b/resources/lang/tr/settings.php index 4b015c042..22ec0965b 100755 --- a/resources/lang/tr/settings.php +++ b/resources/lang/tr/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Geri Dönüşüm Kutusu', 'recycle_bin_desc' => 'Burada silinen öğeleri geri yükleyebilir veya bunları sistemden kalıcı olarak kaldırmayı seçebilirsiniz. Bu liste, izin filtrelerinin uygulandığı sistemdeki benzer etkinlik listelerinden farklı olarak filtrelenmez.', 'recycle_bin_deleted_item' => 'Silinen öge', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Tarafından silindi', 'recycle_bin_deleted_at' => 'Silinme Zamanı', 'recycle_bin_permanently_delete' => 'Kalıcı Olarak Sil', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Geri Yüklenecek Öğeler', 'recycle_bin_restore_confirm' => 'Bu eylem, tüm alt öğeler dahil olmak üzere silinen öğeyi orijinal konumlarına geri yükleyecektir. Orijinal konum o zamandan beri silinmişse ve şimdi geri dönüşüm kutusunda bulunuyorsa, üst öğenin de geri yüklenmesi gerekecektir.', 'recycle_bin_restore_deleted_parent' => 'Bu öğenin üst öğesi de silindi. Bunlar, üst öğe de geri yüklenene kadar silinmiş olarak kalacaktır.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Rol Detayları', 'role_name' => 'Rol Adı', 'role_desc' => 'Rolün Kısa Tanımı', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Harici Doğrulama Kimlikleri', 'role_system' => 'Sistem Yetkileri', 'role_manage_users' => 'Kullanıcıları yönet', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Sayfa şablonlarını yönet', 'role_access_api' => 'Sistem programlama arayüzüne (API) eriş', 'role_manage_settings' => 'Uygulama ayarlarını yönet', + 'role_export_content' => 'Export content', 'role_asset' => 'Varlık Yetkileri', 'roles_system_warning' => 'Yukarıdaki üç izinden herhangi birine erişimin, kullanıcının kendi ayrıcalıklarını veya sistemdeki diğerlerinin ayrıcalıklarını değiştirmesine izin verebileceğini unutmayın. Yalnızca bu izinlere sahip rolleri güvenilir kullanıcılara atayın.', 'role_asset_desc' => 'Bu izinler, sistem içindeki varlıklara varsayılan erişim izinlerini ayarlar. Kitaplar, bölümler ve sayfalar üzerindeki izinler, buradaki izinleri geçersiz kılar.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Anahtar Oluştur', 'users_api_tokens_expires' => 'Bitiş süresi', 'users_api_tokens_docs' => 'API Dokümantasyonu', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'API Anahtarı Oluştur', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/tr/validation.php b/resources/lang/tr/validation.php index 48bbef92b..9cd8093d4 100644 --- a/resources/lang/tr/validation.php +++ b/resources/lang/tr/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute sadece harf, rakam ve tirelerden oluşabilir.', 'alpha_num' => ':attribute sadece harflerden ve rakamlardan oluşabilir.', 'array' => ':attribute bir dizi olmalıdır.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute tarihi, :date tarihinden önceki bir tarih olmalıdır.', 'between' => [ 'numeric' => ':attribute değeri, :min ve :max değerleri arasında olmalıdır.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute, string olmalıdır.', 'timezone' => ':attribute, geçerli bir bölge olmalıdır.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute zaten alınmış.', 'url' => ':attribute formatı geçersiz.', 'uploaded' => 'Dosya yüklemesi başarısız oldu. Sunucu, bu boyuttaki dosyaları kabul etmiyor olabilir.', diff --git a/resources/lang/uk/activities.php b/resources/lang/uk/activities.php index 918eb1a41..88130b90f 100644 --- a/resources/lang/uk/activities.php +++ b/resources/lang/uk/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'прокоментував', 'permissions_update' => 'оновив дозволи', diff --git a/resources/lang/uk/auth.php b/resources/lang/uk/auth.php index e11848a20..52625b60f 100644 --- a/resources/lang/uk/auth.php +++ b/resources/lang/uk/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Ласкаво просимо до :appName!', 'user_invite_page_text' => 'Для завершення процесу створення облікового запису та отримання доступу вам потрібно задати пароль, який буде використовуватися для входу в :appName в майбутньому.', 'user_invite_page_confirm_button' => 'Підтвердити пароль', - 'user_invite_success' => 'Встановлено пароль, тепер у вас є доступ до :appName!' + 'user_invite_success' => 'Встановлено пароль, тепер у вас є доступ до :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/uk/common.php b/resources/lang/uk/common.php index 62342a723..734c566e5 100644 --- a/resources/lang/uk/common.php +++ b/resources/lang/uk/common.php @@ -39,6 +39,7 @@ return [ 'reset' => 'Скинути', 'remove' => 'Видалити', 'add' => 'Додати', + 'configure' => 'Configure', 'fullscreen' => 'На весь екран', 'favourite' => 'Favourite', 'unfavourite' => 'Unfavourite', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Немає активності для показу', 'no_items' => 'Немає доступних елементів', 'back_to_top' => 'Повернутися до початку', + 'skip_to_main_content' => 'Skip to main content', 'toggle_details' => 'Подробиці', 'toggle_thumbnails' => 'Мініатюри', 'details' => 'Деталі', diff --git a/resources/lang/uk/entities.php b/resources/lang/uk/entities.php index f41d97bb0..427acc5b1 100644 --- a/resources/lang/uk/entities.php +++ b/resources/lang/uk/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Вбудований веб-файл', 'export_pdf' => 'PDF файл', 'export_text' => 'Текстовий файл', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Дозволи', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Дозволи на книжкову полицю', 'shelves_permissions_updated' => 'Дозволи на книжкову полицю оновлено', 'shelves_permissions_active' => 'Діючі дозволи на книжкову полицю', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Копіювати дозволи на книги', 'shelves_copy_permissions' => 'Копіювати дозволи', 'shelves_copy_permissions_explain' => 'Це застосовує поточні налаштування дозволів цієї книжкової полиці до всіх книг, що містяться всередині. Перш ніж активувати, переконайтесь що будь-які зміни дозволів цієї книжкової полиці були збережені.', diff --git a/resources/lang/uk/settings.php b/resources/lang/uk/settings.php index fdcd67034..be9c5bb44 100644 --- a/resources/lang/uk/settings.php +++ b/resources/lang/uk/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Кошик', 'recycle_bin_desc' => 'Тут ви можете відновити видалені елементи, або назавжди видалити їх із системи. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.', 'recycle_bin_deleted_item' => 'Виадлений елемент', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Ким видалено', 'recycle_bin_deleted_at' => 'Час видалення', 'recycle_bin_permanently_delete' => 'Видалити остаточно', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Елементи для відновлення', 'recycle_bin_restore_confirm' => 'Ця дія відновить видалений елемент у початкове місце, включаючи всі дочірні елементи. Якщо вихідне розташування відтоді було видалено, і знаходиться у кошику, батьківський елемент також потрібно буде відновити.', 'recycle_bin_restore_deleted_parent' => 'Батьківський елемент цього об\'єкта також був видалений. Вони залишатимуться видаленими, доки батьківський елемент також не буде відновлений.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Видалено :count елементів із кошика.', 'recycle_bin_restore_notification' => 'Відновлено :count елементів із кошика.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Деталі ролі', 'role_name' => 'Назва ролі', 'role_desc' => 'Короткий опис ролі', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Зовнішні ID автентифікації', 'role_system' => 'Системні дозволи', 'role_manage_users' => 'Керування користувачами', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Управління шаблонами сторінок', 'role_access_api' => 'Доступ до системного API', 'role_manage_settings' => 'Керування налаштуваннями програми', + 'role_export_content' => 'Export content', 'role_asset' => 'Дозволи', 'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.', 'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Створити токен', 'users_api_tokens_expires' => 'Закінчується', 'users_api_tokens_docs' => 'Документація API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Створити токен API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/uk/validation.php b/resources/lang/uk/validation.php index 77df1ed4e..a2c1b9890 100644 --- a/resources/lang/uk/validation.php +++ b/resources/lang/uk/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => 'Поле :attribute має містити лише літери, цифри, дефіси та підкреслення.', 'alpha_num' => 'Поле :attribute має містити лише літери та цифри.', 'array' => 'Поле :attribute має бути масивом.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => 'Поле :attribute має містити дату не пізніше :date.', 'between' => [ 'numeric' => 'Поле :attribute має бути між :min та :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => 'Поле :attribute повинне містити текст.', 'timezone' => 'Поле :attribute повинне містити коректну часову зону.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => 'Вказане значення поля :attribute вже існує.', 'url' => 'Формат поля :attribute неправильний.', 'uploaded' => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.', diff --git a/resources/lang/vi/activities.php b/resources/lang/vi/activities.php index 10d24a16b..d5d5e76b3 100644 --- a/resources/lang/vi/activities.php +++ b/resources/lang/vi/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" has been added to your favourites', 'favourite_remove_notification' => '":name" has been removed from your favourites', + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', + // Other 'commented_on' => 'đã bình luận về', 'permissions_update' => 'các quyền đã được cập nhật', diff --git a/resources/lang/vi/auth.php b/resources/lang/vi/auth.php index 5ba2db390..fbb99b830 100644 --- a/resources/lang/vi/auth.php +++ b/resources/lang/vi/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => 'Chào mừng đến với :appName!', 'user_invite_page_text' => 'Để hoàn tất tài khoản và lấy quyền truy cập bạn cần đặt mật khẩu để sử dụng cho các lần đăng nhập sắp tới tại :appName.', 'user_invite_page_confirm_button' => 'Xác nhận Mật khẩu', - 'user_invite_success' => 'Mật khẩu đã được thiết lập, bạn có quyền truy cập đến :appName!' + 'user_invite_success' => 'Mật khẩu đã được thiết lập, bạn có quyền truy cập đến :appName!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/vi/common.php b/resources/lang/vi/common.php index 6e779da24..dd721d994 100644 --- a/resources/lang/vi/common.php +++ b/resources/lang/vi/common.php @@ -39,11 +39,12 @@ return [ 'reset' => 'Thiết lập lại', 'remove' => 'Xóa bỏ', 'add' => 'Thêm', + 'configure' => 'Configure', 'fullscreen' => 'Toàn màn hình', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Yêu thích', + 'unfavourite' => 'Bỏ yêu thích', + 'next' => 'Tiếp theo', + 'previous' => 'Trước đó', // Sort Options 'sort_options' => 'Tùy Chọn Sắp Xếp', @@ -51,7 +52,7 @@ return [ 'sort_ascending' => 'Sắp xếp tăng dần', 'sort_descending' => 'Sắp xếp giảm dần', 'sort_name' => 'Tên', - 'sort_default' => 'Default', + 'sort_default' => 'Mặc định', 'sort_created_at' => 'Ngày Tạo', 'sort_updated_at' => 'Ngày cập nhật', @@ -60,6 +61,7 @@ return [ 'no_activity' => 'Không có hoạt động nào', 'no_items' => 'Không có mục nào khả dụng', 'back_to_top' => 'Lên đầu trang', + 'skip_to_main_content' => 'Nhảy đến nội dung chính', 'toggle_details' => 'Bật/tắt chi tiết', 'toggle_thumbnails' => 'Bật/tắt ảnh ảnh nhỏ', 'details' => 'Chi tiết', @@ -88,6 +90,6 @@ return [ // Footer Link Options // Not directly used but available for convenience to users. - 'privacy_policy' => 'Privacy Policy', - 'terms_of_service' => 'Terms of Service', + 'privacy_policy' => 'Chính Sách Quyền Riêng Tư', + 'terms_of_service' => 'Điều khoản Dịch vụ', ]; diff --git a/resources/lang/vi/entities.php b/resources/lang/vi/entities.php index cfea0956e..5b04e8abf 100644 --- a/resources/lang/vi/entities.php +++ b/resources/lang/vi/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Đang chứa tệp tin Web', 'export_pdf' => 'Tệp PDF', 'export_text' => 'Tệp văn bản thuần túy', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Quyền', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => 'Các quyền đối với kệ sách', 'shelves_permissions_updated' => 'Các quyền với kệ sách đã được cập nhật', 'shelves_permissions_active' => 'Đang bật các quyền hạn từ Kệ sách', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => 'Sao chép các quyền cho sách', 'shelves_copy_permissions' => 'Sao chép các quyền', 'shelves_copy_permissions_explain' => 'Điều này sẽ áp dụng các cài đặt quyền của giá sách hiện tại với tất cả các cuốn sách bên trong. Trước khi kích hoạt, đảm bảo bất cứ thay đổi liên quan đến quyền của giá sách này đã được lưu.', diff --git a/resources/lang/vi/errors.php b/resources/lang/vi/errors.php index 0c4a6fa8f..cfd2b9746 100644 --- a/resources/lang/vi/errors.php +++ b/resources/lang/vi/errors.php @@ -83,9 +83,9 @@ return [ '404_page_not_found' => 'Không Tìm Thấy Trang', 'sorry_page_not_found' => 'Xin lỗi, Không tìm thấy trang bạn đang tìm kiếm.', 'sorry_page_not_found_permission_warning' => 'Nếu trang bạn tìm kiếm tồn tại, có thể bạn đang không có quyền truy cập.', - 'image_not_found' => 'Image Not Found', - 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', - 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', + 'image_not_found' => 'Không tìm thấy Ảnh', + 'image_not_found_subtitle' => 'Rất tiếc, không thể tìm thấy Ảnh bạn đang tìm kiếm.', + 'image_not_found_details' => 'Nếu bạn hi vọng ảnh này tồn tại, rất có thể nó đã bị xóa.', 'return_home' => 'Quay lại trang chủ', 'error_occurred' => 'Đã xảy ra lỗi', 'app_down' => ':appName hiện đang ngoại tuyến', diff --git a/resources/lang/vi/settings.php b/resources/lang/vi/settings.php index a105e82c7..7854268af 100644 --- a/resources/lang/vi/settings.php +++ b/resources/lang/vi/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => 'Thùng Rác', 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', 'recycle_bin_deleted_item' => 'Mục Đã Xóa', + 'recycle_bin_deleted_parent' => 'Parent', 'recycle_bin_deleted_by' => 'Xóa Bởi', 'recycle_bin_deleted_at' => 'Thời điểm Xóa', 'recycle_bin_permanently_delete' => 'Xóa Vĩnh viễn', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => 'Items to be Restored', 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', + 'recycle_bin_restore_parent' => 'Restore Parent', 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', @@ -136,6 +138,7 @@ return [ 'role_details' => 'Thông tin chi tiết Quyền', 'role_name' => 'Tên quyền', 'role_desc' => 'Thông tin vắn tắt của Quyền', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'Mã của xác thực ngoài', 'role_system' => 'Quyền Hệ thống', 'role_manage_users' => 'Quản lý người dùng', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => 'Quản lý các mẫu trang', 'role_access_api' => 'Truy cập đến API hệ thống', 'role_manage_settings' => 'Quản lý cài đặt của ứng dụng', + 'role_export_content' => 'Export content', 'role_asset' => 'Quyền tài sản (asset)', 'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.', 'role_asset_desc' => 'Các quyền này điều khiển truy cập mặc định tới tài sản (asset) nằm trong hệ thống. Quyền tại Sách, Chường và Trang se ghi đè các quyền này.', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => 'Tạo Token', 'users_api_tokens_expires' => 'Hết hạn', 'users_api_tokens_docs' => 'Tài liệu API', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => 'Tạo Token API', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/vi/validation.php b/resources/lang/vi/validation.php index bcfb178fb..7e237cf9e 100644 --- a/resources/lang/vi/validation.php +++ b/resources/lang/vi/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute chỉ được chứa chữ cái, chữ số, gạch nối và gạch dưới.', 'alpha_num' => ':attribute chỉ được chứa chữ cái hoặc chữ số.', 'array' => ':attribute phải là một mảng.', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute phải là một ngày trước :date.', 'between' => [ 'numeric' => ':attribute phải nằm trong khoảng :min đến :max.', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute phải là một chuỗi.', 'timezone' => ':attribute phải là một khu vực hợp lệ.', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute đã có người sử dụng.', 'url' => 'Định dạng của :attribute không hợp lệ.', 'uploaded' => 'Tệp tin đã không được tải lên. Máy chủ không chấp nhận các tệp tin với dung lượng lớn như tệp tin trên.', diff --git a/resources/lang/zh_CN/activities.php b/resources/lang/zh_CN/activities.php index cc453abf6..65e7c3583 100644 --- a/resources/lang/zh_CN/activities.php +++ b/resources/lang/zh_CN/activities.php @@ -47,6 +47,10 @@ return [ 'favourite_add_notification' => '":name" 已添加到你的收藏', 'favourite_remove_notification' => '":name" 已从你的收藏中删除', + // MFA + 'mfa_setup_method_notification' => '多重身份认证设置成功', + 'mfa_remove_method_notification' => '多重身份认证已成功移除', + // Other 'commented_on' => '评论', 'permissions_update' => '权限已更新', diff --git a/resources/lang/zh_CN/auth.php b/resources/lang/zh_CN/auth.php index b883e0b6c..ae5978fd6 100644 --- a/resources/lang/zh_CN/auth.php +++ b/resources/lang/zh_CN/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => '欢迎来到 :appName!', 'user_invite_page_text' => '要完成您的帐户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。', 'user_invite_page_confirm_button' => '确认密码', - 'user_invite_success' => '已设置密码,您现在可以访问 :appName!' + 'user_invite_success' => '已设置密码,您现在可以访问 :appName!', + + // Multi-factor Authentication + 'mfa_setup' => '设置多重身份认证', + 'mfa_setup_desc' => '设置多重身份认证能增加您账户的安全性。', + 'mfa_setup_configured' => '已经设置过了', + 'mfa_setup_reconfigure' => '重新配置', + 'mfa_setup_remove_confirmation' => '您确定想要移除多重身份认证吗?', + 'mfa_setup_action' => '设置', + 'mfa_backup_codes_usage_limit_warning' => '您剩余的备用验证码少于 5 个,请在用完验证码之前生成并保存新的验证码,以防止您的帐户被锁定。', + 'mfa_option_totp_title' => '移动设备 App', + 'mfa_option_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。', + 'mfa_option_backup_codes_title' => '备用验证码', + 'mfa_option_backup_codes_desc' => '安全地保存一组一次性使用的备用验证码,您可以输入这些验证码来验证您的身份。', + 'mfa_gen_confirm_and_enable' => '确认并启用', + 'mfa_gen_backup_codes_title' => '备用验证码设置', + 'mfa_gen_backup_codes_desc' => '将下面的验证码存放在一个安全的地方。访问系统时,您可以使用其中的一个验证码进行二次认证。', + 'mfa_gen_backup_codes_download' => '下载验证码', + 'mfa_gen_backup_codes_usage_warning' => '每个验证码只能使用一次', + 'mfa_gen_totp_title' => '移动设备 App', + 'mfa_gen_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。', + 'mfa_gen_totp_scan' => '要开始操作,请使用你的身份验证 App 扫描下面的二维码。', + 'mfa_gen_totp_verify_setup' => '验证设置', + 'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的验证码来验证一切是否正常:', + 'mfa_gen_totp_provide_code_here' => '再此输入您的 App 生成的验证码', + 'mfa_verify_access' => '认证访问', + 'mfa_verify_access_desc' => '您的账户要求您在访问前通过额外的验证确认您的身份。使用您设置的认证方法认证以继续。', + 'mfa_verify_no_methods' => '没有设置认证方法', + 'mfa_verify_no_methods_desc' => '您的账户没有设置多重身份认证。您需要至少设置一种才能访问。', + 'mfa_verify_use_totp' => '使用移动设备 App 进行认证', + 'mfa_verify_use_backup_codes' => '使用备用验证码进行认证', + 'mfa_verify_backup_code' => '备用验证码', + 'mfa_verify_backup_code_desc' => '在下面输入你的其中一个备用验证码:', + 'mfa_verify_backup_code_enter_here' => '在这里输入备用验证码', + 'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的验证码:', + 'mfa_setup_login_notification' => '多重身份认证已设置,请使用新配置的方法重新登录。', ]; \ No newline at end of file diff --git a/resources/lang/zh_CN/common.php b/resources/lang/zh_CN/common.php index c8a0eef07..6c2fa668b 100644 --- a/resources/lang/zh_CN/common.php +++ b/resources/lang/zh_CN/common.php @@ -39,9 +39,10 @@ return [ 'reset' => '重置', 'remove' => '删除', 'add' => '添加', + 'configure' => '配置', 'fullscreen' => '全屏', 'favourite' => '收藏', - 'unfavourite' => '不喜欢', + 'unfavourite' => '取消收藏', 'next' => '下一页', 'previous' => '上一页', @@ -60,6 +61,7 @@ return [ 'no_activity' => '没有活动要显示', 'no_items' => '没有可用的项目', 'back_to_top' => '回到顶部', + 'skip_to_main_content' => '跳转到主要内容', 'toggle_details' => '显示/隐藏详细信息', 'toggle_thumbnails' => '显示/隐藏缩略图', 'details' => '详细信息', diff --git a/resources/lang/zh_CN/entities.php b/resources/lang/zh_CN/entities.php index 494ac717f..277bcfb32 100644 --- a/resources/lang/zh_CN/entities.php +++ b/resources/lang/zh_CN/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => '网页文件', 'export_pdf' => 'PDF文件', 'export_text' => '纯文本文件', + 'export_md' => 'Markdown 文件', // Permissions and restrictions 'permissions' => '权限', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => '书架权限', 'shelves_permissions_updated' => '书架权限已更新', 'shelves_permissions_active' => '书架权限激活', + 'shelves_permissions_cascade_warning' => '书架上的权限不会自动应用到书架里的书。这是因为书可以在多个书架上存在。使用下面的选项可以将权限复制到书架里的书上。', 'shelves_copy_permissions_to_books' => '将权限复制到图书', 'shelves_copy_permissions' => '复制权限', 'shelves_copy_permissions_explain' => '这会将此书架的当前权限设置应用于其中包含的所有图书。 在激活之前,请确保已保存对此书架权限的任何更改。', @@ -106,7 +108,7 @@ return [ // Books 'book' => '图书', 'books' => '图书', - 'x_books' => ':count本书', + 'x_books' => ':count 本书', 'books_empty' => '不存在已创建的书', 'books_popular' => '热门图书', 'books_recent' => '最近的书', diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php index b6152425f..7de5c87fc 100755 --- a/resources/lang/zh_CN/settings.php +++ b/resources/lang/zh_CN/settings.php @@ -15,7 +15,7 @@ return [ 'app_customization' => '定制', 'app_features_security' => '功能与安全', 'app_name' => '站点名称', - 'app_name_desc' => '此名称将在网页头部和Email中显示。', + 'app_name_desc' => '此名称将在网页头部和系统发送的电子邮件中显示。', 'app_name_header' => '在网页头部显示站点名称?', 'app_public_access' => '访问权限', 'app_public_access_desc' => '启用此选项将允许未登录的用户访问站点内容。', @@ -35,7 +35,7 @@ return [ 'app_primary_color' => '站点主色', 'app_primary_color_desc' => '这应该是一个十六进制值。
      保留为空以重置为默认颜色。', 'app_homepage' => '站点主页', - 'app_homepage_desc' => '选择要在主页上显示的页面来替换默认的视图,选定页面的访问权限将被忽略。', + 'app_homepage_desc' => '选择要在主页上显示的页面来替换默认的页面,选定页面的访问权限将被忽略。', 'app_homepage_select' => '选择一个页面', 'app_footer_links' => '页脚链接', 'app_footer_links_desc' => '添加在网站页脚中显示的链接。这些链接将显示在大多数页面的底部,也包括不需要登录的页面。您可以使用标签"trans::"来使用系统定义的翻译。例如:使用"trans::common.privacy_policy"将显示为“隐私政策”,而"trans::common.terms_of_service"将显示为“服务条款”。', @@ -64,15 +64,15 @@ return [ 'reg_enable_external_warning' => '当启用外部LDAP或者SAML认证时,上面的选项会被忽略。当使用外部系统认证认证成功时,将自动创建非现有会员的用户账户。', 'reg_email_confirmation' => '邮件确认', 'reg_email_confirmation_toggle' => '需要电子邮件确认', - 'reg_confirm_email_desc' => '如果使用域名限制,则需要Email验证,并且该值将被忽略。', + 'reg_confirm_email_desc' => '如果使用域名限制,则需要电子邮件验证,并且该值将被忽略。', 'reg_confirm_restrict_domain' => '域名限制', - 'reg_confirm_restrict_domain_desc' => '输入您想要限制注册的Email域名列表,用逗号隔开。在被允许与应用程序交互之前,用户将被发送一封Email来确认他们的地址。
      注意用户在注册成功后可以修改他们的Email地址。', + 'reg_confirm_restrict_domain_desc' => '输入您想要限制注册的电子邮件域名列表(即只允许使用这些电子邮件域名注册),多个域名用英文逗号隔开。在允许用户与应用程序交互之前,系统将向用户发送一封电子邮件以确认其电子邮件地址。
      请注意,用户在注册成功后仍然可以更改他们的电子邮件地址。', 'reg_confirm_restrict_domain_placeholder' => '尚未设置限制', // Maintenance settings 'maint' => '维护', 'maint_image_cleanup' => '清理图像', - 'maint_image_cleanup_desc' => "扫描页面和修订内容以检查哪些图像是正在使用的以及哪些图像是多余的。确保在运行前创建完整的数据库和映像备份。", + 'maint_image_cleanup_desc' => "扫描页面和修订内容以检查哪些图片是正在使用的以及哪些图片是多余的。确保在运行前完整备份数据库和图片。", 'maint_delete_images_only_in_revisions' => '同时删除只存在于旧的页面修订中的图片', 'maint_image_cleanup_run' => '运行清理', 'maint_image_cleanup_warning' => '发现了 :count 张可能未使用的图像。您确定要删除这些图像吗?', @@ -92,6 +92,7 @@ return [ 'recycle_bin' => '回收站', 'recycle_bin_desc' => '在这里,您可以还原已删除的项目,或选择将其从系统中永久删除。与系统中过滤过的类似的活动记录不同,这个表会显示所有操作。', 'recycle_bin_deleted_item' => '被删除的项目', + 'recycle_bin_deleted_parent' => '上级', 'recycle_bin_deleted_by' => '删除者', 'recycle_bin_deleted_at' => '删除时间', 'recycle_bin_permanently_delete' => '永久删除', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => '要恢复的项目', 'recycle_bin_restore_confirm' => '此操作会将已删除的项目及其所有子元素恢复到原始位置。如果项目的原始位置已被删除,并且现在位于回收站中,则要恢复项目的上级项目也需要恢复。', 'recycle_bin_restore_deleted_parent' => '该项目的上级项目也已被删除。这些项目将保持被删除状态,直到上级项目被恢复。', + 'recycle_bin_restore_parent' => '还原上级', 'recycle_bin_destroy_notification' => '从回收站中删除了 :count 个项目。', 'recycle_bin_restore_notification' => '从回收站中恢复了 :count 个项目。', @@ -136,6 +138,7 @@ return [ 'role_details' => '角色详细信息', 'role_name' => '角色名', 'role_desc' => '角色简述', + 'role_mfa_enforced' => '需要多重身份认证', 'role_external_auth_id' => '外部身份认证ID', 'role_system' => '系统权限', 'role_manage_users' => '管理用户', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => '管理页面模板', 'role_access_api' => '访问系统 API', 'role_manage_settings' => '管理App设置', + 'role_export_content' => '导出内容', 'role_asset' => '资源许可', 'roles_system_warning' => '请注意,具有上述三个权限中的任何一个都可以允许用户更改自己的特权或系统中其他人的特权。 只将具有这些权限的角色分配给受信任的用户。', 'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍,章节和页面上的权限将覆盖这里的权限设定。', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => '创建令牌', 'users_api_tokens_expires' => '过期', 'users_api_tokens_docs' => 'API文档', + 'users_mfa' => '多重身份认证', + 'users_mfa_desc' => '设置多重身份认证能增加您账户的安全性。', + 'users_mfa_x_methods' => ':count 方法已配置|:count 方法已配置', + 'users_mfa_configure' => '配置方法', // API Tokens 'user_api_token_create' => '创建 API 令牌', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => '挪威语 (Bokmål)', diff --git a/resources/lang/zh_CN/validation.php b/resources/lang/zh_CN/validation.php index 72b0d594e..6163ca751 100644 --- a/resources/lang/zh_CN/validation.php +++ b/resources/lang/zh_CN/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute 只能包含字母、数字和短横线。', 'alpha_num' => ':attribute 只能包含字母和数字。', 'array' => ':attribute 必须是一个数组。', + 'backup_codes' => '您输入的验证码无效或已被使用。', 'before' => ':attribute 必须是在 :date 前的日期。', 'between' => [ 'numeric' => ':attribute 必须在:min到:max之间。', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute 必须是字符串。', 'timezone' => ':attribute 必须是有效的区域。', + 'totp' => '您输入的验证码无效或已过期。', 'unique' => ':attribute 已经被使用。', 'url' => ':attribute 格式无效。', 'uploaded' => '无法上传文件。 服务器可能不接受此大小的文件。', diff --git a/resources/lang/zh_TW/activities.php b/resources/lang/zh_TW/activities.php index 6d07a176a..0fe3ecd84 100644 --- a/resources/lang/zh_TW/activities.php +++ b/resources/lang/zh_TW/activities.php @@ -44,8 +44,12 @@ return [ 'bookshelf_delete_notification' => '書架已刪除成功', // Favourites - 'favourite_add_notification' => '":name" has been added to your favourites', - 'favourite_remove_notification' => '":name" has been removed from your favourites', + 'favourite_add_notification' => '":name" 已加入到你的最愛', + 'favourite_remove_notification' => '":name" 已從你的最愛移除', + + // MFA + 'mfa_setup_method_notification' => 'Multi-factor method successfully configured', + 'mfa_remove_method_notification' => 'Multi-factor method successfully removed', // Other 'commented_on' => '評論', diff --git a/resources/lang/zh_TW/auth.php b/resources/lang/zh_TW/auth.php index e4f3c7978..89fc6f55a 100644 --- a/resources/lang/zh_TW/auth.php +++ b/resources/lang/zh_TW/auth.php @@ -73,5 +73,40 @@ return [ 'user_invite_page_welcome' => '歡迎使用 :appName!', 'user_invite_page_text' => '要完成設定您的帳號並取得存取權,您必須設定密碼,此密碼將用於登入 :appName。', 'user_invite_page_confirm_button' => '確認密碼', - 'user_invite_success' => '密碼已設定,您現在可以存取 :appName 了!' + 'user_invite_success' => '密碼已設定,您現在可以存取 :appName 了!', + + // Multi-factor Authentication + 'mfa_setup' => 'Setup Multi-Factor Authentication', + 'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'mfa_setup_configured' => 'Already configured', + 'mfa_setup_reconfigure' => 'Reconfigure', + 'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?', + 'mfa_setup_action' => 'Setup', + 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_option_totp_title' => 'Mobile App', + 'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Backup Codes', + 'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.', + 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', + 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', + 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', + 'mfa_gen_backup_codes_download' => 'Download Codes', + 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', + 'mfa_gen_totp_title' => 'Mobile App Setup', + 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', + 'mfa_gen_totp_verify_setup' => 'Verify Setup', + 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', + 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', + 'mfa_verify_access' => 'Verify Access', + 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', + 'mfa_verify_no_methods' => 'No Methods Configured', + 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', + 'mfa_verify_use_totp' => 'Verify using a mobile app', + 'mfa_verify_use_backup_codes' => 'Verify using a backup code', + 'mfa_verify_backup_code' => 'Backup Code', + 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', + 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', + 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', ]; \ No newline at end of file diff --git a/resources/lang/zh_TW/common.php b/resources/lang/zh_TW/common.php index 6d1d15f3e..b358111fd 100644 --- a/resources/lang/zh_TW/common.php +++ b/resources/lang/zh_TW/common.php @@ -39,11 +39,12 @@ return [ 'reset' => '重設', 'remove' => '移除', 'add' => '新增', + 'configure' => 'Configure', 'fullscreen' => '全螢幕', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => '最愛', + 'unfavourite' => '取消最愛', + 'next' => '下一頁', + 'previous' => '上一頁', // Sort Options 'sort_options' => '排序選項', @@ -51,7 +52,7 @@ return [ 'sort_ascending' => '遞增排序', 'sort_descending' => '遞減排序', 'sort_name' => '名稱', - 'sort_default' => 'Default', + 'sort_default' => '預設', 'sort_created_at' => '建立日期', 'sort_updated_at' => '更新日期', @@ -60,6 +61,7 @@ return [ 'no_activity' => '無活動可顯示', 'no_items' => '無可用項目', 'back_to_top' => '回到頂端', + 'skip_to_main_content' => '跳到主內容', 'toggle_details' => '顯示/隱藏詳細資訊', 'toggle_thumbnails' => '顯示/隱藏縮圖', 'details' => '詳細資訊', @@ -69,7 +71,7 @@ return [ 'breadcrumb' => '頁面路徑', // Header - 'header_menu_expand' => 'Expand Header Menu', + 'header_menu_expand' => '展開選單', 'profile_menu' => '個人資料選單', 'view_profile' => '檢視個人資料', 'edit_profile' => '編輯個人資料', @@ -78,9 +80,9 @@ return [ // Layout tabs 'tab_info' => '資訊', - 'tab_info_label' => 'Tab: Show Secondary Information', + 'tab_info_label' => '顯示次要訊息', 'tab_content' => '內容', - 'tab_content_label' => 'Tab: Show Primary Content', + 'tab_content_label' => '顯示主要內容', // Email Content 'email_action_help' => '如果您無法點擊 ":actionText" 按鈕,請將下方的網址複製並貼上到您的網路瀏覽器中:', diff --git a/resources/lang/zh_TW/entities.php b/resources/lang/zh_TW/entities.php index 32e3792be..2b98bddb8 100644 --- a/resources/lang/zh_TW/entities.php +++ b/resources/lang/zh_TW/entities.php @@ -27,8 +27,8 @@ return [ 'images' => '圖片', 'my_recent_drafts' => '我最近的草稿', 'my_recently_viewed' => '我最近檢視', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => '我瀏覽最多次的最愛', + 'my_favourites' => '我的最愛', 'no_pages_viewed' => '您尚未看過任何頁面', 'no_pages_recently_created' => '最近未建立任何頁面', 'no_pages_recently_updated' => '最近沒有頁面被更新', @@ -36,6 +36,7 @@ return [ 'export_html' => '網頁檔案', 'export_pdf' => 'PDF 檔案', 'export_text' => '純文字檔案', + 'export_md' => 'Markdown 檔案', // Permissions and restrictions 'permissions' => '權限', @@ -62,7 +63,7 @@ return [ 'search_permissions_set' => '權限設定', 'search_created_by_me' => '我建立的', 'search_updated_by_me' => '我更新的', - 'search_owned_by_me' => 'Owned by me', + 'search_owned_by_me' => '我所擁有的', 'search_date_options' => '日期選項', 'search_updated_before' => '在此之前更新', 'search_updated_after' => '在此之後更新', @@ -98,6 +99,7 @@ return [ 'shelves_permissions' => '書架權限', 'shelves_permissions_updated' => '書架權限已更新', 'shelves_permissions_active' => '書架權限已啟用', + 'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', 'shelves_copy_permissions_to_books' => '將權限複製到書本', 'shelves_copy_permissions' => '複製權限', 'shelves_copy_permissions_explain' => '這會將此書架目前的權限設定套用到所有包含的書本上。在啟用前,請確認您已儲存任何對此書架權限的變更。', diff --git a/resources/lang/zh_TW/errors.php b/resources/lang/zh_TW/errors.php index 0435fc8ad..0d898552f 100644 --- a/resources/lang/zh_TW/errors.php +++ b/resources/lang/zh_TW/errors.php @@ -83,9 +83,9 @@ return [ '404_page_not_found' => '找不到頁面', 'sorry_page_not_found' => '抱歉,找不到您在尋找的頁面。', 'sorry_page_not_found_permission_warning' => '如果您確認這個頁面存在,則代表可能沒有查看它的權限。', - 'image_not_found' => 'Image Not Found', - 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', - 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', + 'image_not_found' => '找不到圖片', + 'image_not_found_subtitle' => '對不起,無法找到您所看的圖片', + 'image_not_found_details' => '原本的圖片可能已經被刪除', 'return_home' => '回到首頁', 'error_occurred' => '發生錯誤', 'app_down' => ':appName 離線中', diff --git a/resources/lang/zh_TW/settings.php b/resources/lang/zh_TW/settings.php index 77c1d8b83..839a6c719 100644 --- a/resources/lang/zh_TW/settings.php +++ b/resources/lang/zh_TW/settings.php @@ -92,6 +92,7 @@ return [ 'recycle_bin' => '資源回收桶', 'recycle_bin_desc' => '在這裡,您可以還原已刪除的項目,或是選擇將其從系統中永久移除。與系統中套用了權限過濾條件類似的活動列表不同的是,此列表並未過濾。', 'recycle_bin_deleted_item' => '已刪除項目', + 'recycle_bin_deleted_parent' => '上層', 'recycle_bin_deleted_by' => '刪除由', 'recycle_bin_deleted_at' => '刪除時間', 'recycle_bin_permanently_delete' => '永久刪除', @@ -104,6 +105,7 @@ return [ 'recycle_bin_restore_list' => '要被還原的項目', 'recycle_bin_restore_confirm' => '此動作將會還原已被刪除的項目(包含任何下層元素)到其原始位置。如果原始位置已被刪除,且目前位於垃圾桶裡,那麼上層項目也需要被還原。', 'recycle_bin_restore_deleted_parent' => '此項目的上層項目也已被刪除。因此將會保持被刪除的狀態,直到上層項目也被還原。', + 'recycle_bin_restore_parent' => '還原上層', 'recycle_bin_destroy_notification' => '已從回收桶刪除共 :count 個項目。', 'recycle_bin_restore_notification' => '已從回收桶還原共 :count 個項目。', @@ -136,6 +138,7 @@ return [ 'role_details' => '角色詳細資訊', 'role_name' => '角色名稱', 'role_desc' => '角色簡短說明', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => '外部身份驗證 ID', 'role_system' => '系統權限', 'role_manage_users' => '管理使用者', @@ -145,6 +148,7 @@ return [ 'role_manage_page_templates' => '管理頁面範本', 'role_access_api' => '存取系統 API', 'role_manage_settings' => '管理應用程式設定', + 'role_export_content' => 'Export content', 'role_asset' => '資源權限', 'roles_system_warning' => '請注意,有上述三項權限中的任一項的使用者都可以更改自己或系統中其他人的權限。有這些權限的角色只應分配給受信任的使用者。', 'role_asset_desc' => '對系統內資源的預設權限將由這裡的權限控制。若有單獨設定在書本、章節和頁面上的權限,將會覆寫這裡的權限設定。', @@ -202,6 +206,10 @@ return [ 'users_api_tokens_create' => '建立權杖', 'users_api_tokens_expires' => '過期', 'users_api_tokens_docs' => 'API 文件', + 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', + 'users_mfa_x_methods' => ':count method configured|:count methods configured', + 'users_mfa_configure' => 'Configure Methods', // API Tokens 'user_api_token_create' => '建立 API 權杖', @@ -247,6 +255,7 @@ return [ 'it' => 'Italian', 'ja' => '日本語', 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', 'lv' => 'Latviešu Valoda', 'nl' => 'Nederlands', 'nb' => 'Norsk (Bokmål)', diff --git a/resources/lang/zh_TW/validation.php b/resources/lang/zh_TW/validation.php index 691ebb619..e93c182ee 100644 --- a/resources/lang/zh_TW/validation.php +++ b/resources/lang/zh_TW/validation.php @@ -15,6 +15,7 @@ return [ 'alpha_dash' => ':attribute 只能包含字母、數字、破折號與底線。', 'alpha_num' => ':attribute 只能包含字母和數字。', 'array' => ':attribute 必須是陣列。', + 'backup_codes' => 'The provided code is not valid or has already been used.', 'before' => ':attribute 必須是在 :date 前的日期。', 'between' => [ 'numeric' => ':attribute 必須在 :min 到 :max 之間。', @@ -98,6 +99,7 @@ return [ ], 'string' => ':attribute 必須是字元串。', 'timezone' => ':attribute 必須是有效的區域。', + 'totp' => 'The provided code is not valid or has expired.', 'unique' => ':attribute 已經被使用。', 'url' => ':attribute 格式無效。', 'uploaded' => '無法上傳文件, 服務器可能不接受此大小的文件。', diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 953d1d060..665b1213b 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -29,6 +29,10 @@ } } +.input-fill-width { + width: 100% !important; +} + .fake-input { @extend .input-base; overflow: auto; @@ -115,6 +119,7 @@ .markdown-editor-display { background-color: #fff; body { + display: block; background-color: #fff; padding-inline-start: 16px; padding-inline-end: 16px; diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 516d7d612..e26948301 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -181,6 +181,10 @@ body.flexbox { display: inline-block !important; } +.relative { + position: relative; +} + .hidden { display: none !important; } diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss index 7a0987c66..cbe3cd4be 100644 --- a/resources/sass/_text.scss +++ b/resources/sass/_text.scss @@ -280,13 +280,9 @@ ul, ol { } } ul { - padding-left: $-m * 1.3; - padding-right: $-m * 1.3; list-style: disc; ul { list-style: circle; - margin-top: 0; - margin-bottom: 0; } label { margin: 0; @@ -295,23 +291,33 @@ ul { ol { list-style: decimal; - padding-left: $-m * 2; - padding-right: $-m * 2; +} + +ol, ul { + padding-left: $-m * 2.0; + padding-right: $-m * 2.0; +} + +li > ol, li > ul { + margin-top: 0; + margin-bottom: 0; + margin-block-end: 0; + margin-block-start: 0; + padding-block-end: 0; + padding-block-start: 0; + padding-left: $-m * 1.2; + padding-right: $-m * 1.2; } li.checkbox-item, li.task-list-item { list-style: none; - margin-left: - ($-m * 1.3); + margin-left: -($-m * 1.2); input[type="checkbox"] { margin-right: $-xs; } -} - -li > ol, li > ul { - margin-block-end: 0px; - margin-block-start: 0px; - padding-block-end: 0px; - padding-block-start: 0px; + li.checkbox-item, li.task-list-item { + margin-left: $-xs; + } } /* diff --git a/resources/views/api-docs/index.blade.php b/resources/views/api-docs/index.blade.php index 56f7135c3..5bec265e8 100644 --- a/resources/views/api-docs/index.blade.php +++ b/resources/views/api-docs/index.blade.php @@ -1,4 +1,4 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body') @@ -37,148 +37,7 @@
      -

      Getting Started

      - -
      Authentication
      -

      - To access the API a user has to have the "Access System API" permission enabled on one of their assigned roles. - Permissions to content accessed via the API is limited by the roles & permissions assigned to the user that's used to access the API. -

      -

      Authentication to use the API is primarily done using API Tokens. Once the "Access System API" permission has been assigned to a user, a "API Tokens" section should be visible when editing their user profile. Choose "Create Token" and enter an appropriate name and expiry date, relevant for your API usage then press "Save". A "Token ID" and "Token Secret" will be immediately displayed. These values should be used as a header in API HTTP requests in the following format:

      -
      Authorization: Token <token_id>:<token_secret>
      -

      Here's an example of an authorized cURL request to list books in the system:

      -
      curl --request GET \
      -  --url https://example.com/api/books \
      -  --header 'Authorization: Token C6mdvEQTGnebsmVn3sFNeeuelGEBjyQp:NOvD3VlzuSVuBPNaf1xWHmy7nIRlaj22'
      -

      If already logged into the system within the browser, via a user account with permission to access the API, the system will also accept an existing session meaning you can browse API endpoints directly in the browser or use the browser devtools to play with the API.

      - -
      - -
      Request Format
      -

      The API is primarily design to be interfaced using JSON so the majority of API endpoints, that accept data, will read JSON request data although application/x-www-form-urlencoded request data is also accepted. Endpoints that receive file data will need data sent in a multipart/form-data format although this will be highlighted in the documentation for such endpoints.

      -

      For endpoints in this documentation that accept data, a "Body Parameters" table will be available showing the parameters that will accepted in the request. Any rules for the values of such parameters, such as the data-type or if they're required, will be shown alongside the parameter name.

      - -
      - -
      Listing Endpoints
      -

      Some endpoints will return a list of data models. These endpoints will return an array of the model data under a data property along with a numeric total property to indicate the total number of records found for the query within the system. Here's an example of a listing response:

      -
      {
      -  "data": [
      -    {
      -      "id": 1,
      -      "name": "BookStack User Guide",
      -      "slug": "bookstack-user-guide",
      -      "description": "This is a general guide on using BookStack on a day-to-day basis.",
      -      "created_at": "2019-05-05 21:48:46",
      -      "updated_at": "2019-12-11 20:57:31",
      -      "created_by": 1,
      -      "updated_by": 1,
      -      "image_id": 3
      -    }
      -  ],
      -  "total": 16
      -}
      -

      - There are a number of standard URL parameters that can be supplied to manipulate and page through the results returned from a listing endpoint: -

      - - - - - - - - - - - - - - - - - - - - - - - - - - -
      ParameterDetailsExamples
      count - Specify how many records will be returned in the response.
      - (Default: {{ config('api.default_item_count') }}, Max: {{ config('api.max_item_count') }}) -
      Limit the count to 50
      ?count=50
      offset - Specify how many records to skip over in the response.
      - (Default: 0) -
      Skip over the first 100 records
      ?offset=100
      sort - Specify what field is used to sort the data and the direction of the sort (Ascending or Descending).
      - Value is the name of a field, A + or - prefix dictates ordering.
      - Direction defaults to ascending.
      - Can use most fields shown in the response. -
      - Sort by name ascending
      ?sort=+name

      - Sort by "Created At" date descending
      ?sort=-created_at -
      filter[<field>] - Specify a filter to be applied to the query. Can use most fields shown in the response.
      - By default a filter will apply a "where equals" query but the below operations are available using the format filter[<field>:<operation>]
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      eqWhere <field> equals the filter value.
      neWhere <field> does not equal the filter value.
      gtWhere <field> is greater than the filter value.
      ltWhere <field> is less than the filter value.
      gteWhere <field> is greater than or equal to the filter value.
      lteWhere <field> is less than or equal to the filter value.
      like - Where <field> is "like" the filter value.
      - % symbols can be used as wildcards. -
      -
      - Filter where id is 5:
      ?filter[id]=5

      - Filter where id is not 5:
      ?filter[id:ne]=5

      - Filter where name contains "cat":
      ?filter[name:like]=%cat%

      - Filter where created after 2020-01-01:
      ?filter[created_at:gt]=2020-01-01 -
      - -
      - -
      Error Handling
      -

      - Successful responses will return a 200 or 204 HTTP response code. Errors will return a 4xx or a 5xx HTTP response code depending on the type of error. Errors follow a standard format as shown below. The message provided may be translated depending on the configured language of the system in addition to the API users' language preference. The code provided in the JSON response will match the HTTP response code. -

      - -
      {
      -	"error": {
      -		"code": 401,
      -		"message": "No authorization token found on the request"
      -	}
      -}
      -
      - + @include('api-docs.parts.getting-started')
      @foreach($docs as $model => $endpoints) @@ -186,52 +45,7 @@

      {{ $model }}

      @foreach($endpoints as $endpoint) -
      {{ $endpoint['controller_method_kebab'] }}
      -
      - {{ $endpoint['method'] }} - @if($endpoint['controller_method_kebab'] === 'list') - {{ url($endpoint['uri']) }} - @else - {{ url($endpoint['uri']) }} - @endif -
      -

      {{ $endpoint['description'] ?? '' }}

      - @if($endpoint['body_params'] ?? false) -
      - Body Parameters - - - - - - @foreach($endpoint['body_params'] as $paramName => $rules) - - - - - @endforeach -
      Param NameValue Rules
      {{ $paramName }} - @foreach($rules as $rule) - {{ $rule }} - @endforeach -
      -
      - @endif - @if($endpoint['example_request'] ?? false) -
      - Example Request -
      {{ $endpoint['example_request'] }}
      -
      - @endif - @if($endpoint['example_response'] ?? false) -
      - Example Response -
      {{ $endpoint['example_response'] }}
      -
      - @endif - @if(!$loop->last) -
      - @endif + @include('api-docs.parts.endpoint', ['endpoint' => $endpoint, 'loop' => $loop]) @endforeach @endforeach diff --git a/resources/views/api-docs/parts/endpoint.blade.php b/resources/views/api-docs/parts/endpoint.blade.php new file mode 100644 index 000000000..c1bce805b --- /dev/null +++ b/resources/views/api-docs/parts/endpoint.blade.php @@ -0,0 +1,52 @@ +
      {{ $endpoint['controller_method_kebab'] }}
      + +
      + {{ $endpoint['method'] }} + @if($endpoint['controller_method_kebab'] === 'list') + {{ url($endpoint['uri']) }} + @else + {{ url($endpoint['uri']) }} + @endif +
      + +

      {{ $endpoint['description'] ?? '' }}

      + +@if($endpoint['body_params'] ?? false) +
      + Body Parameters + + + + + + @foreach($endpoint['body_params'] as $paramName => $rules) + + + + + @endforeach +
      Param NameValue Rules
      {{ $paramName }} + @foreach($rules as $rule) + {{ $rule }} + @endforeach +
      +
      +@endif + +@if($endpoint['example_request'] ?? false) +
      + Example Request +
      {{ $endpoint['example_request'] }}
      +
      +@endif + +@if($endpoint['example_response'] ?? false) +
      + Example Response +
      {{ $endpoint['example_response'] }}
      +
      +@endif + +@if(!$loop->last) +
      +@endif \ No newline at end of file diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php new file mode 100644 index 000000000..ba0f85fc7 --- /dev/null +++ b/resources/views/api-docs/parts/getting-started.blade.php @@ -0,0 +1,141 @@ +

      Getting Started

      + +
      Authentication
      +

      + To access the API a user has to have the "Access System API" permission enabled on one of their assigned roles. + Permissions to content accessed via the API is limited by the roles & permissions assigned to the user that's used to access the API. +

      +

      Authentication to use the API is primarily done using API Tokens. Once the "Access System API" permission has been assigned to a user, a "API Tokens" section should be visible when editing their user profile. Choose "Create Token" and enter an appropriate name and expiry date, relevant for your API usage then press "Save". A "Token ID" and "Token Secret" will be immediately displayed. These values should be used as a header in API HTTP requests in the following format:

      +
      Authorization: Token <token_id>:<token_secret>
      +

      Here's an example of an authorized cURL request to list books in the system:

      +
      curl --request GET \
      +  --url https://example.com/api/books \
      +  --header 'Authorization: Token C6mdvEQTGnebsmVn3sFNeeuelGEBjyQp:NOvD3VlzuSVuBPNaf1xWHmy7nIRlaj22'
      +

      If already logged into the system within the browser, via a user account with permission to access the API, the system will also accept an existing session meaning you can browse API endpoints directly in the browser or use the browser devtools to play with the API.

      + +
      + +
      Request Format
      +

      The API is primarily design to be interfaced using JSON so the majority of API endpoints, that accept data, will read JSON request data although application/x-www-form-urlencoded request data is also accepted. Endpoints that receive file data will need data sent in a multipart/form-data format although this will be highlighted in the documentation for such endpoints.

      +

      For endpoints in this documentation that accept data, a "Body Parameters" table will be available showing the parameters that will accepted in the request. Any rules for the values of such parameters, such as the data-type or if they're required, will be shown alongside the parameter name.

      + +
      + +
      Listing Endpoints
      +

      Some endpoints will return a list of data models. These endpoints will return an array of the model data under a data property along with a numeric total property to indicate the total number of records found for the query within the system. Here's an example of a listing response:

      +
      {
      +  "data": [
      +    {
      +      "id": 1,
      +      "name": "BookStack User Guide",
      +      "slug": "bookstack-user-guide",
      +      "description": "This is a general guide on using BookStack on a day-to-day basis.",
      +      "created_at": "2019-05-05 21:48:46",
      +      "updated_at": "2019-12-11 20:57:31",
      +      "created_by": 1,
      +      "updated_by": 1,
      +      "image_id": 3
      +    }
      +  ],
      +  "total": 16
      +}
      +

      + There are a number of standard URL parameters that can be supplied to manipulate and page through the results returned from a listing endpoint: +

      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      ParameterDetailsExamples
      count + Specify how many records will be returned in the response.
      + (Default: {{ config('api.default_item_count') }}, Max: {{ config('api.max_item_count') }}) +
      Limit the count to 50
      ?count=50
      offset + Specify how many records to skip over in the response.
      + (Default: 0) +
      Skip over the first 100 records
      ?offset=100
      sort + Specify what field is used to sort the data and the direction of the sort (Ascending or Descending).
      + Value is the name of a field, A + or - prefix dictates ordering.
      + Direction defaults to ascending.
      + Can use most fields shown in the response. +
      + Sort by name ascending
      ?sort=+name

      + Sort by "Created At" date descending
      ?sort=-created_at +
      filter[<field>] + Specify a filter to be applied to the query. Can use most fields shown in the response.
      + By default a filter will apply a "where equals" query but the below operations are available using the format filter[<field>:<operation>]
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      eqWhere <field> equals the filter value.
      neWhere <field> does not equal the filter value.
      gtWhere <field> is greater than the filter value.
      ltWhere <field> is less than the filter value.
      gteWhere <field> is greater than or equal to the filter value.
      lteWhere <field> is less than or equal to the filter value.
      like + Where <field> is "like" the filter value.
      + % symbols can be used as wildcards. +
      +
      + Filter where id is 5:
      ?filter[id]=5

      + Filter where id is not 5:
      ?filter[id:ne]=5

      + Filter where name contains "cat":
      ?filter[name:like]=%cat%

      + Filter where created after 2020-01-01:
      ?filter[created_at:gt]=2020-01-01 +
      + +
      + +
      Error Handling
      +

      + Successful responses will return a 200 or 204 HTTP response code. Errors will return a 4xx or a 5xx HTTP response code depending on the type of error. Errors follow a standard format as shown below. The message provided may be translated depending on the configured language of the system in addition to the API users' language preference. The code provided in the JSON response will match the HTTP response code. +

      + +
      {
      +	"error": {
      +		"code": 401,
      +		"message": "No authorization token found on the request"
      +	}
      +}
      +
      \ No newline at end of file diff --git a/resources/views/attachments/manager-edit-form.blade.php b/resources/views/attachments/manager-edit-form.blade.php index ee86dc240..15837448a 100644 --- a/resources/views/attachments/manager-edit-form.blade.php +++ b/resources/views/attachments/manager-edit-form.blade.php @@ -22,7 +22,7 @@
      - @include('components.dropzone', [ + @include('form.dropzone', [ 'placeholder' => trans('entities.attachments_edit_drop_upload'), 'url' => url('/attachments/upload/' . $attachment->id), 'successMessage' => trans('entities.attachments_file_updated'), diff --git a/resources/views/attachments/manager.blade.php b/resources/views/attachments/manager.blade.php index 4628f7495..024cb583c 100644 --- a/resources/views/attachments/manager.blade.php +++ b/resources/views/attachments/manager.blade.php @@ -18,7 +18,7 @@ @include('attachments.manager-list', ['attachments' => $page->attachments->all()])
    diff --git a/resources/views/partials/loading-icon.blade.php b/resources/views/common/loading-icon.blade.php similarity index 100% rename from resources/views/partials/loading-icon.blade.php rename to resources/views/common/loading-icon.blade.php diff --git a/resources/views/partials/notifications.blade.php b/resources/views/common/notifications.blade.php similarity index 100% rename from resources/views/partials/notifications.blade.php rename to resources/views/common/notifications.blade.php diff --git a/resources/views/common/parts/skip-to-content.blade.php b/resources/views/common/skip-to-content.blade.php similarity index 100% rename from resources/views/common/parts/skip-to-content.blade.php rename to resources/views/common/skip-to-content.blade.php diff --git a/resources/views/partials/book-tree.blade.php b/resources/views/entities/book-tree.blade.php similarity index 76% rename from resources/views/partials/book-tree.blade.php rename to resources/views/entities/book-tree.blade.php index 15b583289..ce016143a 100644 --- a/resources/views/partials/book-tree.blade.php +++ b/resources/views/entities/book-tree.blade.php @@ -7,19 +7,19 @@ - \ No newline at end of file + diff --git a/resources/views/partials/entity-export-meta.blade.php b/resources/views/entities/export-meta.blade.php similarity index 100% rename from resources/views/partials/entity-export-meta.blade.php rename to resources/views/entities/export-meta.blade.php diff --git a/resources/views/partials/entity-favourite-action.blade.php b/resources/views/entities/favourite-action.blade.php similarity index 100% rename from resources/views/partials/entity-favourite-action.blade.php rename to resources/views/entities/favourite-action.blade.php diff --git a/resources/views/partials/entity-grid-item.blade.php b/resources/views/entities/grid-item.blade.php similarity index 100% rename from resources/views/partials/entity-grid-item.blade.php rename to resources/views/entities/grid-item.blade.php diff --git a/resources/views/partials/entity-list-basic.blade.php b/resources/views/entities/list-basic.blade.php similarity index 76% rename from resources/views/partials/entity-list-basic.blade.php rename to resources/views/entities/list-basic.blade.php index dc5c3f333..84b910484 100644 --- a/resources/views/partials/entity-list-basic.blade.php +++ b/resources/views/entities/list-basic.blade.php @@ -1,7 +1,7 @@
    @if(count($entities) > 0) @foreach($entities as $index => $entity) - @include('partials.entity-list-item-basic', ['entity' => $entity]) + @include('entities.list-item-basic', ['entity' => $entity]) @endforeach @else

    diff --git a/resources/views/partials/entity-list-item-basic.blade.php b/resources/views/entities/list-item-basic.blade.php similarity index 100% rename from resources/views/partials/entity-list-item-basic.blade.php rename to resources/views/entities/list-item-basic.blade.php diff --git a/resources/views/partials/entity-list-item.blade.php b/resources/views/entities/list-item.blade.php similarity index 79% rename from resources/views/partials/entity-list-item.blade.php rename to resources/views/entities/list-item.blade.php index d605953c7..8b5eb20b0 100644 --- a/resources/views/partials/entity-list-item.blade.php +++ b/resources/views/entities/list-item.blade.php @@ -1,4 +1,4 @@ -@component('partials.entity-list-item-basic', ['entity' => $entity]) +@component('entities.list-item-basic', ['entity' => $entity])

    @@ -16,7 +16,7 @@ @if(($showTags ?? false) && $entity->tags->count() > 0)
    - @include('components.tag-list', ['entity' => $entity, 'linked' => false ]) + @include('entities.tag-list', ['entity' => $entity, 'linked' => false ])
    @endif diff --git a/resources/views/partials/entity-list.blade.php b/resources/views/entities/list.blade.php similarity index 63% rename from resources/views/partials/entity-list.blade.php rename to resources/views/entities/list.blade.php index 393f4e8a7..25673c583 100644 --- a/resources/views/partials/entity-list.blade.php +++ b/resources/views/entities/list.blade.php @@ -1,7 +1,7 @@ @if(count($entities) > 0)
    @foreach($entities as $index => $entity) - @include('partials.entity-list-item', ['entity' => $entity, 'showPath' => $showPath ?? false, 'showTags' => $showTags ?? false]) + @include('entities.list-item', ['entity' => $entity, 'showPath' => $showPath ?? false, 'showTags' => $showTags ?? false]) @endforeach
    @else diff --git a/resources/views/partials/entity-meta.blade.php b/resources/views/entities/meta.blade.php similarity index 100% rename from resources/views/partials/entity-meta.blade.php rename to resources/views/entities/meta.blade.php diff --git a/resources/views/partials/entity-search-form.blade.php b/resources/views/entities/search-form.blade.php similarity index 100% rename from resources/views/partials/entity-search-form.blade.php rename to resources/views/entities/search-form.blade.php diff --git a/resources/views/partials/entity-search-results.blade.php b/resources/views/entities/search-results.blade.php similarity index 91% rename from resources/views/partials/entity-search-results.blade.php rename to resources/views/entities/search-results.blade.php index 74619831a..a3c4aa8f7 100644 --- a/resources/views/partials/entity-search-results.blade.php +++ b/resources/views/entities/search-results.blade.php @@ -9,7 +9,7 @@
    - @include('partials.loading-icon') + @include('common.loading-icon')
    \ No newline at end of file diff --git a/resources/views/components/entity-selector-popup.blade.php b/resources/views/entities/selector-popup.blade.php similarity index 88% rename from resources/views/components/entity-selector-popup.blade.php rename to resources/views/entities/selector-popup.blade.php index ec8712b6a..ab73a014f 100644 --- a/resources/views/components/entity-selector-popup.blade.php +++ b/resources/views/entities/selector-popup.blade.php @@ -5,7 +5,7 @@ - @include('components.entity-selector', ['name' => 'entity-selector']) + @include('entities.selector', ['name' => 'entity-selector']) diff --git a/resources/views/components/entity-selector.blade.php b/resources/views/entities/selector.blade.php similarity index 94% rename from resources/views/components/entity-selector.blade.php rename to resources/views/entities/selector.blade.php index c71fdff63..687392dea 100644 --- a/resources/views/components/entity-selector.blade.php +++ b/resources/views/entities/selector.blade.php @@ -5,7 +5,7 @@ option:entity-selector:entity-permission="{{ $entityPermission ?? 'view' }}"> -
    @include('partials.loading-icon')
    +
    @include('common.loading-icon')
    @if($showAdd ?? false)
    diff --git a/resources/views/partials/entity-sibling-navigation.blade.php b/resources/views/entities/sibling-navigation.blade.php similarity index 100% rename from resources/views/partials/entity-sibling-navigation.blade.php rename to resources/views/entities/sibling-navigation.blade.php diff --git a/resources/views/partials/sort.blade.php b/resources/views/entities/sort.blade.php similarity index 100% rename from resources/views/partials/sort.blade.php rename to resources/views/entities/sort.blade.php diff --git a/resources/views/components/tag-list.blade.php b/resources/views/entities/tag-list.blade.php similarity index 100% rename from resources/views/components/tag-list.blade.php rename to resources/views/entities/tag-list.blade.php diff --git a/resources/views/components/tag-manager-list.blade.php b/resources/views/entities/tag-manager-list.blade.php similarity index 100% rename from resources/views/components/tag-manager-list.blade.php rename to resources/views/entities/tag-manager-list.blade.php diff --git a/resources/views/components/tag-manager.blade.php b/resources/views/entities/tag-manager.blade.php similarity index 84% rename from resources/views/components/tag-manager.blade.php rename to resources/views/entities/tag-manager.blade.php index 9e24ba3fd..5975c4bd9 100644 --- a/resources/views/components/tag-manager.blade.php +++ b/resources/views/entities/tag-manager.blade.php @@ -9,7 +9,7 @@
    - @include('components.tag-manager-list', ['tags' => $entity ? $entity->tags->all() : []]) + @include('entities.tag-manager-list', ['tags' => $entity ? $entity->tags->all() : []])
    diff --git a/resources/views/partials/view-toggle.blade.php b/resources/views/entities/view-toggle.blade.php similarity index 100% rename from resources/views/partials/view-toggle.blade.php rename to resources/views/entities/view-toggle.blade.php diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php index c4a5dc782..a4e5a0dd4 100644 --- a/resources/views/errors/404.blade.php +++ b/resources/views/errors/404.blade.php @@ -1,4 +1,4 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('content')
    @@ -28,7 +28,7 @@

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

    - @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact']) + @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['page']), 'style' => 'compact'])
    @@ -36,7 +36,7 @@

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

    - @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact']) + @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['book']), 'style' => 'compact'])
    @@ -44,7 +44,7 @@

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

    - @include('partials.entity-list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact']) + @include('entities.list', ['entities' => (new \BookStack\Entities\Queries\Popular)->run(10, 0, ['chapter']), 'style' => 'compact'])
    diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php index ad759b49d..d7d58e4c4 100644 --- a/resources/views/errors/500.blade.php +++ b/resources/views/errors/500.blade.php @@ -1,4 +1,4 @@ -@extends('base') +@extends('layouts.base') @section('content') diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php index 29364606b..9f86bfdc6 100644 --- a/resources/views/errors/503.blade.php +++ b/resources/views/errors/503.blade.php @@ -1,4 +1,4 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('content') diff --git a/resources/views/form/checkbox.blade.php b/resources/views/form/checkbox.blade.php index 255b591aa..fb2d4e259 100644 --- a/resources/views/form/checkbox.blade.php +++ b/resources/views/form/checkbox.blade.php @@ -4,7 +4,7 @@ $label $errors? $model? --}} -@include('components.custom-checkbox', [ +@include('form.custom-checkbox', [ 'name' => $name, 'label' => $label, 'value' => 'true', diff --git a/resources/views/components/custom-checkbox.blade.php b/resources/views/form/custom-checkbox.blade.php similarity index 100% rename from resources/views/components/custom-checkbox.blade.php rename to resources/views/form/custom-checkbox.blade.php diff --git a/resources/views/components/dropzone.blade.php b/resources/views/form/dropzone.blade.php similarity index 100% rename from resources/views/components/dropzone.blade.php rename to resources/views/form/dropzone.blade.php diff --git a/resources/views/form/entity-permissions.blade.php b/resources/views/form/entity-permissions.blade.php index d3e89cc44..ed04bc041 100644 --- a/resources/views/form/entity-permissions.blade.php +++ b/resources/views/form/entity-permissions.blade.php @@ -15,11 +15,15 @@
    - @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by', 'compact' => false]) + @include('form.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by', 'compact' => false])
    + @if($model instanceof \BookStack\Entities\Models\Bookshelf) +

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

    + @endif +
    diff --git a/resources/views/components/image-picker.blade.php b/resources/views/form/image-picker.blade.php similarity index 100% rename from resources/views/components/image-picker.blade.php rename to resources/views/form/image-picker.blade.php diff --git a/resources/views/form/restriction-checkbox.blade.php b/resources/views/form/restriction-checkbox.blade.php index 65a94239e..02c477f4a 100644 --- a/resources/views/form/restriction-checkbox.blade.php +++ b/resources/views/form/restriction-checkbox.blade.php @@ -5,7 +5,7 @@ $role $action $model? --}} -@include('components.custom-checkbox', [ +@include('form.custom-checkbox', [ 'name' => $name . '[' . $role->id . '][' . $action . ']', 'label' => $label, 'value' => 'true', diff --git a/resources/views/form/role-checkboxes.blade.php b/resources/views/form/role-checkboxes.blade.php index fc6ad93a8..7e5ca629a 100644 --- a/resources/views/form/role-checkboxes.blade.php +++ b/resources/views/form/role-checkboxes.blade.php @@ -2,7 +2,7 @@
    @foreach($roles as $role)
    - @include('components.custom-checkbox', [ + @include('form.custom-checkbox', [ 'name' => $name . '[' . strval($role->id) . ']', 'label' => $role->display_name, 'value' => $role->id, diff --git a/resources/views/components/toggle-switch.blade.php b/resources/views/form/toggle-switch.blade.php similarity index 100% rename from resources/views/components/toggle-switch.blade.php rename to resources/views/form/toggle-switch.blade.php diff --git a/resources/views/components/user-select-list.blade.php b/resources/views/form/user-select-list.blade.php similarity index 100% rename from resources/views/components/user-select-list.blade.php rename to resources/views/form/user-select-list.blade.php diff --git a/resources/views/components/user-select.blade.php b/resources/views/form/user-select.blade.php similarity index 96% rename from resources/views/components/user-select.blade.php rename to resources/views/form/user-select.blade.php index 50c731efd..8823bb075 100644 --- a/resources/views/components/user-select.blade.php +++ b/resources/views/form/user-select.blade.php @@ -27,7 +27,7 @@ type="text">
    - @include('partials.loading-icon') + @include('common.loading-icon')
    diff --git a/resources/views/common/home-book.blade.php b/resources/views/home/books.blade.php similarity index 63% rename from resources/views/common/home-book.blade.php rename to resources/views/home/books.blade.php index 9f62d21e7..75d4ae14a 100644 --- a/resources/views/common/home-book.blade.php +++ b/resources/views/home/books.blade.php @@ -1,11 +1,11 @@ -@extends('tri-layout') +@extends('layouts.tri') @section('body') - @include('books.list', ['books' => $books, 'view' => $view]) + @include('books.parts.list', ['books' => $books, 'view' => $view]) @stop @section('left') - @include('common.home-sidebar') + @include('home.parts.sidebar') @stop @section('right') @@ -18,9 +18,9 @@ {{ trans('entities.books_create') }} @endif - @include('partials.view-toggle', ['view' => $view, 'type' => 'books']) - @include('components.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) - @include('partials.dark-mode-toggle', ['classes' => 'icon-list-item text-primary']) + @include('entities.view-toggle', ['view' => $view, 'type' => 'books']) + @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) + @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary']) @stop diff --git a/resources/views/common/home.blade.php b/resources/views/home/default.blade.php similarity index 83% rename from resources/views/common/home.blade.php rename to resources/views/home/default.blade.php index 187f222b5..b8866526d 100644 --- a/resources/views/common/home.blade.php +++ b/resources/views/home/default.blade.php @@ -1,4 +1,4 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body') @@ -6,12 +6,12 @@
    - @include('components.expand-toggle', ['classes' => 'text-muted text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) + @include('home.parts.expand-toggle', ['classes' => 'text-muted text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
    - @include('partials.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-primary']) + @include('common.dark-mode-toggle', ['classes' => 'text-muted icon-list-item text-primary'])
    @@ -24,7 +24,7 @@

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

    - @include('partials.entity-list', ['entities' => $draftPages, 'style' => 'compact']) + @include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
    @endif @@ -32,7 +32,7 @@

    {{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}

    - @include('partials.entity-list', [ + @include('entities.list', [ 'entities' => $recents, 'style' => 'compact', 'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty') @@ -48,7 +48,7 @@ {{ trans('entities.my_most_viewed_favourites') }}
    - @include('partials.entity-list', [ + @include('entities.list', [ 'entities' => $favourites, 'style' => 'compact', ]) @@ -59,7 +59,7 @@

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

    - @include('partials.entity-list', [ + @include('entities.list', [ 'entities' => $recentlyUpdatedPages, 'style' => 'compact', 'emptyText' => trans('entities.no_pages_recently_updated') @@ -72,7 +72,7 @@

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

    - @include('partials.activity-list', ['activity' => $activity]) + @include('common.activity-list', ['activity' => $activity])
    diff --git a/resources/views/components/expand-toggle.blade.php b/resources/views/home/parts/expand-toggle.blade.php similarity index 100% rename from resources/views/components/expand-toggle.blade.php rename to resources/views/home/parts/expand-toggle.blade.php diff --git a/resources/views/common/home-sidebar.blade.php b/resources/views/home/parts/sidebar.blade.php similarity index 80% rename from resources/views/common/home-sidebar.blade.php rename to resources/views/home/parts/sidebar.blade.php index 7fe0659f0..8dc8118f5 100644 --- a/resources/views/common/home-sidebar.blade.php +++ b/resources/views/home/parts/sidebar.blade.php @@ -1,7 +1,7 @@ @if(count($draftPages) > 0)
    {{ trans('entities.my_recent_drafts') }}
    - @include('partials.entity-list', ['entities' => $draftPages, 'style' => 'compact']) + @include('entities.list', ['entities' => $draftPages, 'style' => 'compact'])
    @endif @@ -10,7 +10,7 @@
    {{ trans('entities.my_most_viewed_favourites') }}
    - @include('partials.entity-list', [ + @include('entities.list', [ 'entities' => $favourites, 'style' => 'compact', ]) @@ -19,7 +19,7 @@
    {{ trans('entities.' . (auth()->check() ? 'my_recently_viewed' : 'books_recent')) }}
    - @include('partials.entity-list', [ + @include('entities.list', [ 'entities' => $recents, 'style' => 'compact', 'emptyText' => auth()->check() ? trans('entities.no_pages_viewed') : trans('entities.books_empty') @@ -29,7 +29,7 @@
    {{ trans('entities.recently_updated_pages') }}
    - @include('partials.entity-list', [ + @include('entities.list', [ 'entities' => $recentlyUpdatedPages, 'style' => 'compact', 'emptyText' => trans('entities.no_pages_recently_updated') @@ -39,5 +39,5 @@
    {{ trans('entities.recent_activity') }}
    - @include('partials.activity-list', ['activity' => $activity]) + @include('common.activity-list', ['activity' => $activity])
    \ No newline at end of file diff --git a/resources/views/common/home-shelves.blade.php b/resources/views/home/shelves.blade.php similarity index 63% rename from resources/views/common/home-shelves.blade.php rename to resources/views/home/shelves.blade.php index 3c32fed74..c525643b9 100644 --- a/resources/views/common/home-shelves.blade.php +++ b/resources/views/home/shelves.blade.php @@ -1,11 +1,11 @@ -@extends('tri-layout') +@extends('layouts.tri') @section('body') - @include('shelves.list', ['shelves' => $shelves, 'view' => $view]) + @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view]) @stop @section('left') - @include('common.home-sidebar') + @include('home.parts.sidebar') @stop @section('right') @@ -18,9 +18,9 @@ {{ trans('entities.shelves_new_action') }} @endif - @include('partials.view-toggle', ['view' => $view, 'type' => 'shelves']) - @include('components.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) - @include('partials.dark-mode-toggle', ['classes' => 'icon-list-item text-primary']) + @include('entities.view-toggle', ['view' => $view, 'type' => 'shelves']) + @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) + @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
    @stop diff --git a/resources/views/common/home-custom.blade.php b/resources/views/home/specific-page.blade.php similarity index 62% rename from resources/views/common/home-custom.blade.php rename to resources/views/home/specific-page.blade.php index a22e26002..936433b27 100644 --- a/resources/views/common/home-custom.blade.php +++ b/resources/views/home/specific-page.blade.php @@ -1,25 +1,25 @@ -@extends('tri-layout') +@extends('layouts.tri') @section('body')
    - @include('pages.page-display', ['page' => $customHomepage]) + @include('pages.parts.page-display', ['page' => $customHomepage])
    @stop @section('left') - @include('common.home-sidebar') + @include('home.parts.sidebar') @stop @section('right')
    {{ trans('common.actions') }}
    - @include('components.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) - @include('partials.dark-mode-toggle', ['classes' => 'icon-list-item text-primary']) + @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details']) + @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
    @stop \ No newline at end of file diff --git a/resources/views/base.blade.php b/resources/views/layouts/base.blade.php similarity index 78% rename from resources/views/base.blade.php rename to resources/views/layouts/base.blade.php index 0734466be..6a45b4209 100644 --- a/resources/views/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -11,6 +11,12 @@ + + + + @stack('social-meta') + + @@ -18,8 +24,8 @@ @yield('head') - @include('partials.custom-styles') - @include('partials.custom-head') + @include('common.custom-styles') + @include('common.custom-head') @stack('head') @@ -28,8 +34,8 @@ - @include('common.parts.skip-to-content') - @include('partials.notifications') + @include('common.skip-to-content') + @include('common.notifications') @include('common.header')
    diff --git a/resources/views/export-layout.blade.php b/resources/views/layouts/export.blade.php similarity index 68% rename from resources/views/export-layout.blade.php rename to resources/views/layouts/export.blade.php index f23b3cca5..55df43a45 100644 --- a/resources/views/export-layout.blade.php +++ b/resources/views/layouts/export.blade.php @@ -4,8 +4,8 @@ @yield('title') - @include('partials.export-styles', ['format' => $format]) - @include('partials.export-custom-head') + @include('common.export-styles', ['format' => $format]) + @include('common.export-custom-head')
    diff --git a/resources/views/simple-layout.blade.php b/resources/views/layouts/simple.blade.php similarity index 90% rename from resources/views/simple-layout.blade.php rename to resources/views/layouts/simple.blade.php index b7d6d3ccd..5fb231bdb 100644 --- a/resources/views/simple-layout.blade.php +++ b/resources/views/layouts/simple.blade.php @@ -1,4 +1,4 @@ -@extends('base') +@extends('layouts.base') @section('content') diff --git a/resources/views/tri-layout.blade.php b/resources/views/layouts/tri.blade.php similarity index 98% rename from resources/views/tri-layout.blade.php rename to resources/views/layouts/tri.blade.php index d985db649..e95b21445 100644 --- a/resources/views/tri-layout.blade.php +++ b/resources/views/layouts/tri.blade.php @@ -1,4 +1,4 @@ -@extends('base') +@extends('layouts.base') @section('body-class', 'tri-layout') @section('content-components', 'tri-layout') diff --git a/resources/views/mfa/backup-codes-generate.blade.php b/resources/views/mfa/backup-codes-generate.blade.php new file mode 100644 index 000000000..27ab0317c --- /dev/null +++ b/resources/views/mfa/backup-codes-generate.blade.php @@ -0,0 +1,36 @@ +@extends('layouts.simple') + +@section('body') + +
    +
    +

    {{ trans('auth.mfa_gen_backup_codes_title') }}

    +

    {{ trans('auth.mfa_gen_backup_codes_desc') }}

    + +
    +
    + @foreach($codes as $code) + {{ $code }}
    + @endforeach +
    +
    + +

    + {{ trans('auth.mfa_gen_backup_codes_download') }} +

    + +

    + {{ trans('auth.mfa_gen_backup_codes_usage_warning') }} +

    + +
    + {{ csrf_field() }} +
    + {{ trans('common.cancel') }} + +
    + +
    +
    + +@stop diff --git a/resources/views/mfa/parts/setup-method-row.blade.php b/resources/views/mfa/parts/setup-method-row.blade.php new file mode 100644 index 000000000..e195174c1 --- /dev/null +++ b/resources/views/mfa/parts/setup-method-row.blade.php @@ -0,0 +1,30 @@ +
    +
    +
    {{ trans('auth.mfa_option_' . $method . '_title') }}
    +

    + {{ trans('auth.mfa_option_' . $method . '_desc') }} +

    +
    +
    + @if($userMethods->has($method)) +
    + @icon('check-circle') + {{ trans('auth.mfa_setup_configured') }} +
    + {{ trans('auth.mfa_setup_reconfigure') }} +
    + + +
    + @else + {{ trans('auth.mfa_setup_action') }} + @endif +
    +
    \ No newline at end of file diff --git a/resources/views/mfa/parts/verify-backup_codes.blade.php b/resources/views/mfa/parts/verify-backup_codes.blade.php new file mode 100644 index 000000000..0e5b82086 --- /dev/null +++ b/resources/views/mfa/parts/verify-backup_codes.blade.php @@ -0,0 +1,17 @@ +
    {{ trans('auth.mfa_verify_backup_code') }}
    + +

    {{ trans('auth.mfa_verify_backup_code_desc') }}

    + +
    + {{ csrf_field() }} + + @if($errors->has('code')) +
    {{ $errors->first('code') }}
    + @endif +
    + +
    + \ No newline at end of file diff --git a/resources/views/mfa/parts/verify-totp.blade.php b/resources/views/mfa/parts/verify-totp.blade.php new file mode 100644 index 000000000..9a861fc6c --- /dev/null +++ b/resources/views/mfa/parts/verify-totp.blade.php @@ -0,0 +1,17 @@ +
    {{ trans('auth.mfa_option_totp_title') }}
    + +

    {{ trans('auth.mfa_verify_totp_desc') }}

    + +
    + {{ csrf_field() }} + + @if($errors->has('code')) +
    {{ $errors->first('code') }}
    + @endif +
    + +
    + \ No newline at end of file diff --git a/resources/views/mfa/setup.blade.php b/resources/views/mfa/setup.blade.php new file mode 100644 index 000000000..702f007b7 --- /dev/null +++ b/resources/views/mfa/setup.blade.php @@ -0,0 +1,18 @@ +@extends('layouts.simple') + +@section('body') +
    + +
    +

    {{ trans('auth.mfa_setup') }}

    +

    {{ trans('auth.mfa_setup_desc') }}

    + +
    + @foreach(['totp', 'backup_codes'] as $method) + @include('mfa.parts.setup-method-row', ['method' => $method]) + @endforeach +
    + +
    +
    +@stop diff --git a/resources/views/mfa/totp-generate.blade.php b/resources/views/mfa/totp-generate.blade.php new file mode 100644 index 000000000..f9a7c46ac --- /dev/null +++ b/resources/views/mfa/totp-generate.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.simple') + +@section('body') + +
    +
    +

    {{ trans('auth.mfa_gen_totp_title') }}

    +

    {{ trans('auth.mfa_gen_totp_desc') }}

    +

    {{ trans('auth.mfa_gen_totp_scan') }}

    + +
    +
    + {!! $svg !!} +
    +
    + +

    {{ trans('auth.mfa_gen_totp_verify_setup') }}

    +

    {{ trans('auth.mfa_gen_totp_verify_setup_desc') }}

    +
    + {{ csrf_field() }} + + @if($errors->has('code')) +
    {{ $errors->first('code') }}
    + @endif +
    + {{ trans('common.cancel') }} + +
    + +
    +
    + +@stop diff --git a/resources/views/mfa/verify.blade.php b/resources/views/mfa/verify.blade.php new file mode 100644 index 000000000..3cadeac00 --- /dev/null +++ b/resources/views/mfa/verify.blade.php @@ -0,0 +1,35 @@ +@extends('layouts.simple') + +@section('body') +
    + +
    +

    {{ trans('auth.mfa_verify_access') }}

    +

    {{ trans('auth.mfa_verify_access_desc') }}

    + + @if(!$method) +
    +
    {{ trans('auth.mfa_verify_no_methods') }}
    +

    {{ trans('auth.mfa_verify_no_methods_desc') }}

    + + @endif + + @if($method) +
    + @include('mfa.parts.verify-' . $method) + @endif + + @if(count($otherMethods) > 0) +
    + @foreach($otherMethods as $otherMethod) + + @endforeach + @endif + +
    +
    +@stop diff --git a/resources/views/common/robots.blade.php b/resources/views/misc/robots.blade.php similarity index 100% rename from resources/views/common/robots.blade.php rename to resources/views/misc/robots.blade.php diff --git a/resources/views/pages/copy.blade.php b/resources/views/pages/copy.blade.php index 0f2af0476..2f24d8165 100644 --- a/resources/views/pages/copy.blade.php +++ b/resources/views/pages/copy.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ $page->book, $page->chapter, $page, @@ -33,7 +33,7 @@
    - @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create']) + @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
    diff --git a/resources/views/pages/delete.blade.php b/resources/views/pages/delete.blade.php index 2ec046fa0..39cd07bbb 100644 --- a/resources/views/pages/delete.blade.php +++ b/resources/views/pages/delete.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ $page->book, $page->chapter, $page, diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index 2120bddb2..db518b0d4 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -1,4 +1,4 @@ -@extends('base') +@extends('layouts.base') @section('head') @@ -15,12 +15,12 @@ @if(!isset($isDraft)) @endif - @include('pages.form', ['model' => $page]) - @include('pages.editor-toolbox') + @include('pages.parts.form', ['model' => $page]) + @include('pages.parts.editor-toolbox')
    - @include('components.image-manager', ['uploaded_to' => $page->id]) - @include('components.code-editor') - @include('components.entity-selector-popup') + @include('pages.parts.image-manager', ['uploaded_to' => $page->id]) + @include('pages.parts.code-editor') + @include('entities.selector-popup') @stop \ No newline at end of file diff --git a/resources/views/pages/export.blade.php b/resources/views/pages/export.blade.php index 74d17c128..d2f448d6e 100644 --- a/resources/views/pages/export.blade.php +++ b/resources/views/pages/export.blade.php @@ -1,13 +1,13 @@ -@extends('export-layout') +@extends('layouts.export') @section('title', $page->name) @section('content') - @include('pages.page-display') + @include('pages.parts.page-display')
    - @include('partials.entity-export-meta', ['entity' => $page]) + @include('entities.export-meta', ['entity' => $page])
    @endsection \ No newline at end of file diff --git a/resources/views/pages/guest-create.blade.php b/resources/views/pages/guest-create.blade.php index 55db85144..d6e1cae44 100644 --- a/resources/views/pages/guest-create.blade.php +++ b/resources/views/pages/guest-create.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ ($parent->isA('chapter') ? $parent->book : null), $parent, $parent->getUrl('/create-page') => [ diff --git a/resources/views/pages/move.blade.php b/resources/views/pages/move.blade.php index 26b872cdd..6df36496a 100644 --- a/resources/views/pages/move.blade.php +++ b/resources/views/pages/move.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ $page->book, $page->chapter, $page, @@ -23,7 +23,7 @@ {!! csrf_field() !!} - @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create', 'autofocus' => true]) + @include('entities.selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create', 'autofocus' => true])
    {{ trans('common.cancel') }} diff --git a/resources/views/components/code-editor.blade.php b/resources/views/pages/parts/code-editor.blade.php similarity index 100% rename from resources/views/components/code-editor.blade.php rename to resources/views/pages/parts/code-editor.blade.php diff --git a/resources/views/pages/editor-toolbox.blade.php b/resources/views/pages/parts/editor-toolbox.blade.php similarity index 86% rename from resources/views/pages/editor-toolbox.blade.php rename to resources/views/pages/parts/editor-toolbox.blade.php index 87a9cc2de..f3b54ddcd 100644 --- a/resources/views/pages/editor-toolbox.blade.php +++ b/resources/views/pages/parts/editor-toolbox.blade.php @@ -12,7 +12,7 @@

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

    - @include('components.tag-manager', ['entity' => $page]) + @include('entities.tag-manager', ['entity' => $page])
    @@ -24,7 +24,7 @@

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

    - @include('pages.template-manager', ['page' => $page, 'templates' => $templates]) + @include('pages.parts.template-manager', ['page' => $page, 'templates' => $templates])
    diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/parts/form.blade.php similarity index 97% rename from resources/views/pages/form.blade.php rename to resources/views/pages/parts/form.blade.php index 7e8b2fdd6..f6f0143da 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/parts/form.blade.php @@ -78,12 +78,12 @@ {{--WYSIWYG Editor--}} @if(setting('app-editor') === 'wysiwyg') - @include('pages.wysiwyg-editor', ['model' => $model]) + @include('pages.parts.wysiwyg-editor', ['model' => $model]) @endif {{--Markdown Editor--}} @if(setting('app-editor') === 'markdown') - @include('pages.markdown-editor', ['model' => $model]) + @include('pages.parts.markdown-editor', ['model' => $model]) @endif
    diff --git a/resources/views/components/image-manager-form.blade.php b/resources/views/pages/parts/image-manager-form.blade.php similarity index 100% rename from resources/views/components/image-manager-form.blade.php rename to resources/views/pages/parts/image-manager-form.blade.php diff --git a/resources/views/components/image-manager-list.blade.php b/resources/views/pages/parts/image-manager-list.blade.php similarity index 100% rename from resources/views/components/image-manager-list.blade.php rename to resources/views/pages/parts/image-manager-list.blade.php diff --git a/resources/views/components/image-manager.blade.php b/resources/views/pages/parts/image-manager.blade.php similarity index 98% rename from resources/views/components/image-manager.blade.php rename to resources/views/pages/parts/image-manager.blade.php index 4f03eeaec..c15c31b86 100644 --- a/resources/views/components/image-manager.blade.php +++ b/resources/views/pages/parts/image-manager.blade.php @@ -45,7 +45,7 @@
    - @include('components.dropzone', [ + @include('form.dropzone', [ 'placeholder' => trans('components.image_dropzone'), 'successMessage' => trans('components.image_upload_success'), 'url' => url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0])) diff --git a/resources/views/pages/list-item.blade.php b/resources/views/pages/parts/list-item.blade.php similarity index 60% rename from resources/views/pages/list-item.blade.php rename to resources/views/pages/parts/list-item.blade.php index 1e26cf1d5..5707a9c66 100644 --- a/resources/views/pages/list-item.blade.php +++ b/resources/views/pages/parts/list-item.blade.php @@ -1,4 +1,4 @@ -@component('partials.entity-list-item-basic', ['entity' => $page]) +@component('entities.list-item-basic', ['entity' => $page])

    {{ $page->getExcerpt() }}

    diff --git a/resources/views/pages/markdown-editor.blade.php b/resources/views/pages/parts/markdown-editor.blade.php similarity index 100% rename from resources/views/pages/markdown-editor.blade.php rename to resources/views/pages/parts/markdown-editor.blade.php diff --git a/resources/views/pages/page-display.blade.php b/resources/views/pages/parts/page-display.blade.php similarity index 100% rename from resources/views/pages/page-display.blade.php rename to resources/views/pages/parts/page-display.blade.php diff --git a/resources/views/pages/pointer.blade.php b/resources/views/pages/parts/pointer.blade.php similarity index 100% rename from resources/views/pages/pointer.blade.php rename to resources/views/pages/parts/pointer.blade.php diff --git a/resources/views/pages/template-manager-list.blade.php b/resources/views/pages/parts/template-manager-list.blade.php similarity index 100% rename from resources/views/pages/template-manager-list.blade.php rename to resources/views/pages/parts/template-manager-list.blade.php diff --git a/resources/views/pages/template-manager.blade.php b/resources/views/pages/parts/template-manager.blade.php similarity index 86% rename from resources/views/pages/template-manager.blade.php rename to resources/views/pages/parts/template-manager.blade.php index fbdb70a1b..66d53ae7e 100644 --- a/resources/views/pages/template-manager.blade.php +++ b/resources/views/pages/parts/template-manager.blade.php @@ -3,7 +3,7 @@

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

    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'template', 'value' => old('template', $page->template ? 'true' : 'false') === 'true', 'label' => trans('entities.templates_set_as_template') @@ -20,6 +20,6 @@ @endif
    - @include('pages.template-manager-list', ['templates' => $templates]) + @include('pages.parts.template-manager-list', ['templates' => $templates])
    \ No newline at end of file diff --git a/resources/views/pages/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php similarity index 100% rename from resources/views/pages/wysiwyg-editor.blade.php rename to resources/views/pages/parts/wysiwyg-editor.blade.php diff --git a/resources/views/pages/permissions.blade.php b/resources/views/pages/permissions.blade.php index de28137db..792015e28 100644 --- a/resources/views/pages/permissions.blade.php +++ b/resources/views/pages/permissions.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ $page->book, $page->chapter, $page, diff --git a/resources/views/pages/revision.blade.php b/resources/views/pages/revision.blade.php index 0557b6b1c..b3208c211 100644 --- a/resources/views/pages/revision.blade.php +++ b/resources/views/pages/revision.blade.php @@ -1,10 +1,10 @@ -@extends('tri-layout') +@extends('layouts.tri') @section('left')
    {{ trans('common.details') }}
    - @include('partials.entity-meta', ['entity' => $revision]) + @include('entities.meta', ['entity' => $revision])
    @stop @@ -12,7 +12,7 @@ @section('body')
    - @include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id]) + @include('settings.parts.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id]) {{ $activity->type }} diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index ad03b6c91..c87d84c5e 100644 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -1,9 +1,9 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar-with-version', ['selected' => 'settings']) + @include('settings.parts.navbar-with-version', ['selected' => 'settings'])

    {{ trans('settings.app_features_security') }}

    @@ -25,7 +25,7 @@ @endif
    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'setting-app-public', 'value' => setting('app-public'), 'label' => trans('settings.app_public_access_toggle'), @@ -39,7 +39,7 @@

    {{ trans('settings.app_secure_images_desc') }}

    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'setting-app-secure-images', 'value' => setting('app-secure-images'), 'label' => trans('settings.app_secure_images_toggle'), @@ -53,7 +53,7 @@

    {!! trans('settings.app_disable_comments_desc') !!}

    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'setting-app-disable-comments', 'value' => setting('app-disable-comments'), 'label' => trans('settings.app_disable_comments_toggle'), @@ -85,7 +85,7 @@
    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'setting-app-name-header', 'value' => setting('app-name-header'), 'label' => trans('settings.app_name_header'), @@ -112,7 +112,7 @@

    {!! trans('settings.app_logo_desc') !!}

    - @include('components.image-picker', [ + @include('form.image-picker', [ 'removeName' => 'setting-app-logo', 'removeValue' => 'none', 'defaultImage' => url('/logo.png'), @@ -149,13 +149,13 @@
    - @include('components.setting-entity-color-picker', ['type' => 'bookshelf']) - @include('components.setting-entity-color-picker', ['type' => 'book']) - @include('components.setting-entity-color-picker', ['type' => 'chapter']) + @include('settings.parts.setting-entity-color-picker', ['type' => 'bookshelf']) + @include('settings.parts.setting-entity-color-picker', ['type' => 'book']) + @include('settings.parts.setting-entity-color-picker', ['type' => 'chapter'])
    - @include('components.setting-entity-color-picker', ['type' => 'page']) - @include('components.setting-entity-color-picker', ['type' => 'page-draft']) + @include('settings.parts.setting-entity-color-picker', ['type' => 'page']) + @include('settings.parts.setting-entity-color-picker', ['type' => 'page-draft'])
    @@ -174,7 +174,7 @@ @@ -182,7 +182,7 @@

    {{ trans('settings.app_footer_links_desc') }}

    - @include('settings.footer-links', ['name' => 'setting-app-footer-links', 'value' => setting('app-footer-links', [])]) + @include('settings.parts.footer-links', ['name' => 'setting-app-footer-links', 'value' => setting('app-footer-links', [])])
    @@ -215,7 +215,7 @@

    {!! trans('settings.reg_enable_desc') !!}

    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'setting-registration-enabled', 'value' => setting('registration-enabled'), 'label' => trans('settings.reg_enable_toggle') @@ -255,7 +255,7 @@

    {{ trans('settings.reg_confirm_email_desc') }}

    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'setting-registration-confirmation', 'value' => setting('registration-confirmation'), 'label' => trans('settings.reg_email_confirmation_toggle') @@ -273,5 +273,5 @@
    - @include('components.entity-selector-popup', ['entityTypes' => 'page']) + @include('entities.selector-popup', ['entityTypes' => 'page']) @stop diff --git a/resources/views/settings/maintenance.blade.php b/resources/views/settings/maintenance.blade.php index 941a258d8..ea94413f2 100644 --- a/resources/views/settings/maintenance.blade.php +++ b/resources/views/settings/maintenance.blade.php @@ -1,9 +1,9 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar-with-version', ['selected' => 'maintenance']) + @include('settings.parts.navbar-with-version', ['selected' => 'maintenance'])

    {{ trans('settings.recycle_bin') }}

    diff --git a/resources/views/settings/footer-links.blade.php b/resources/views/settings/parts/footer-links.blade.php similarity index 100% rename from resources/views/settings/footer-links.blade.php rename to resources/views/settings/parts/footer-links.blade.php diff --git a/resources/views/settings/navbar-with-version.blade.php b/resources/views/settings/parts/navbar-with-version.blade.php similarity index 86% rename from resources/views/settings/navbar-with-version.blade.php rename to resources/views/settings/parts/navbar-with-version.blade.php index c02c370fe..09af699a3 100644 --- a/resources/views/settings/navbar-with-version.blade.php +++ b/resources/views/settings/parts/navbar-with-version.blade.php @@ -4,7 +4,7 @@ $version - Version of bookstack to display --}}
    - @include('settings.navbar', ['selected' => $selected]) + @include('settings.parts.navbar', ['selected' => $selected])
    diff --git a/resources/views/settings/navbar.blade.php b/resources/views/settings/parts/navbar.blade.php similarity index 100% rename from resources/views/settings/navbar.blade.php rename to resources/views/settings/parts/navbar.blade.php diff --git a/resources/views/components/page-picker.blade.php b/resources/views/settings/parts/page-picker.blade.php similarity index 100% rename from resources/views/components/page-picker.blade.php rename to resources/views/settings/parts/page-picker.blade.php diff --git a/resources/views/components/setting-entity-color-picker.blade.php b/resources/views/settings/parts/setting-entity-color-picker.blade.php similarity index 100% rename from resources/views/components/setting-entity-color-picker.blade.php rename to resources/views/settings/parts/setting-entity-color-picker.blade.php diff --git a/resources/views/partials/table-user.blade.php b/resources/views/settings/parts/table-user.blade.php similarity index 100% rename from resources/views/partials/table-user.blade.php rename to resources/views/settings/parts/table-user.blade.php diff --git a/resources/views/settings/recycle-bin/deletable-entity-list.blade.php b/resources/views/settings/recycle-bin/deletable-entity-list.blade.php deleted file mode 100644 index 07ad94f8e..000000000 --- a/resources/views/settings/recycle-bin/deletable-entity-list.blade.php +++ /dev/null @@ -1,11 +0,0 @@ -@include('partials.entity-display-item', ['entity' => $entity]) -@if($entity->isA('book')) - @foreach($entity->chapters()->withTrashed()->get() as $chapter) - @include('partials.entity-display-item', ['entity' => $chapter]) - @endforeach -@endif -@if($entity->isA('book') || $entity->isA('chapter')) - @foreach($entity->pages()->withTrashed()->get() as $page) - @include('partials.entity-display-item', ['entity' => $page]) - @endforeach -@endif \ No newline at end of file diff --git a/resources/views/settings/recycle-bin/destroy.blade.php b/resources/views/settings/recycle-bin/destroy.blade.php index bd5ef79f0..ab6034984 100644 --- a/resources/views/settings/recycle-bin/destroy.blade.php +++ b/resources/views/settings/recycle-bin/destroy.blade.php @@ -1,10 +1,10 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'maintenance']) + @include('settings.parts.navbar', ['selected' => 'maintenance'])
    @@ -20,7 +20,7 @@ @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
    {{ trans('settings.recycle_bin_destroy_list') }}
    - @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable]) + @include('settings.recycle-bin.parts.deletable-entity-list', ['entity' => $deletion->deletable]) @endif
    diff --git a/resources/views/settings/recycle-bin/index.blade.php b/resources/views/settings/recycle-bin/index.blade.php index b5de84efa..b31bf02e5 100644 --- a/resources/views/settings/recycle-bin/index.blade.php +++ b/resources/views/settings/recycle-bin/index.blade.php @@ -1,10 +1,10 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'maintenance']) + @include('settings.parts.navbar', ['selected' => 'maintenance'])
    @@ -39,14 +39,15 @@ - + + @if(count($deletions) === 0) - @@ -78,14 +79,24 @@ @endif - + + diff --git a/resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php b/resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php new file mode 100644 index 000000000..c2d8a4266 --- /dev/null +++ b/resources/views/settings/recycle-bin/parts/deletable-entity-list.blade.php @@ -0,0 +1,11 @@ +@include('settings.recycle-bin.parts.entity-display-item', ['entity' => $entity]) +@if($entity->isA('book')) + @foreach($entity->chapters()->withTrashed()->get() as $chapter) + @include('settings.recycle-bin.parts.entity-display-item', ['entity' => $chapter]) + @endforeach +@endif +@if($entity->isA('book') || $entity->isA('chapter')) + @foreach($entity->pages()->withTrashed()->get() as $page) + @include('settings.recycle-bin.parts.entity-display-item', ['entity' => $page]) + @endforeach +@endif \ No newline at end of file diff --git a/resources/views/partials/entity-display-item.blade.php b/resources/views/settings/recycle-bin/parts/entity-display-item.blade.php similarity index 100% rename from resources/views/partials/entity-display-item.blade.php rename to resources/views/settings/recycle-bin/parts/entity-display-item.blade.php diff --git a/resources/views/settings/recycle-bin/restore.blade.php b/resources/views/settings/recycle-bin/restore.blade.php index c888aa8e5..5268bf067 100644 --- a/resources/views/settings/recycle-bin/restore.blade.php +++ b/resources/views/settings/recycle-bin/restore.blade.php @@ -1,16 +1,16 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'maintenance']) + @include('settings.parts.navbar', ['selected' => 'maintenance'])

    {{ trans('settings.recycle_bin_restore') }}

    {{ trans('settings.recycle_bin_restore_confirm') }}

    -
    + {!! csrf_field() !!} {{ trans('common.cancel') }} @@ -19,10 +19,18 @@ @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
    {{ trans('settings.recycle_bin_restore_list') }}
    - @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed()) -

    {{ trans('settings.recycle_bin_restore_deleted_parent') }}

    - @endif - @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable]) +
    + @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed()) +
    {{ trans('settings.recycle_bin_restore_deleted_parent') }}
    + @endif + @if($parentDeletion) + + @endif +
    + + @include('settings.recycle-bin.parts.deletable-entity-list', ['entity' => $deletion->deletable]) @endif
    diff --git a/resources/views/settings/roles/create.blade.php b/resources/views/settings/roles/create.blade.php index df902133f..f2edfa1c5 100644 --- a/resources/views/settings/roles/create.blade.php +++ b/resources/views/settings/roles/create.blade.php @@ -1,15 +1,15 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'roles']) + @include('settings.parts.navbar', ['selected' => 'roles'])
    - @include('settings.roles.form', ['title' => trans('settings.role_create')]) + @include('settings.roles.parts.form', ['title' => trans('settings.role_create')])
    diff --git a/resources/views/settings/roles/delete.blade.php b/resources/views/settings/roles/delete.blade.php index fa7c12b0a..52362461d 100644 --- a/resources/views/settings/roles/delete.blade.php +++ b/resources/views/settings/roles/delete.blade.php @@ -1,10 +1,10 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'roles']) + @include('settings.parts.navbar', ['selected' => 'roles'])
    diff --git a/resources/views/settings/roles/edit.blade.php b/resources/views/settings/roles/edit.blade.php index 0f83bdb0b..e2018d3e9 100644 --- a/resources/views/settings/roles/edit.blade.php +++ b/resources/views/settings/roles/edit.blade.php @@ -1,15 +1,15 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'roles']) + @include('settings.parts.navbar', ['selected' => 'roles'])
    id}") }}" method="POST"> - @include('settings.roles.form', ['model' => $role, 'title' => trans('settings.role_edit'), 'icon' => 'edit']) + @include('settings.roles.parts.form', ['model' => $role, 'title' => trans('settings.role_edit'), 'icon' => 'edit'])
    diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php index 47cd8c920..6c2996787 100644 --- a/resources/views/settings/roles/index.blade.php +++ b/resources/views/settings/roles/index.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('settings.navbar', ['selected' => 'roles']) + @include('settings.parts.navbar', ['selected' => 'roles'])
    @@ -27,7 +27,12 @@ @foreach($roles as $role)
    - + @endforeach diff --git a/resources/views/settings/roles/checkbox.blade.php b/resources/views/settings/roles/parts/checkbox.blade.php similarity index 85% rename from resources/views/settings/roles/checkbox.blade.php rename to resources/views/settings/roles/parts/checkbox.blade.php index 98201da8f..44cdd22bd 100644 --- a/resources/views/settings/roles/checkbox.blade.php +++ b/resources/views/settings/roles/parts/checkbox.blade.php @@ -1,5 +1,5 @@ -@include('components.custom-checkbox', [ +@include('form.custom-checkbox', [ 'name' => 'permissions[' . $permission . ']', 'value' => 'true', 'checked' => old('permissions'.$permission, false)|| (!old('display_name', false) && (isset($role) && $role->hasPermission($permission))), diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/parts/form.blade.php similarity index 51% rename from resources/views/settings/roles/form.blade.php rename to resources/views/settings/roles/parts/form.blade.php index 604acbb16..2f94398b5 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/parts/form.blade.php @@ -11,13 +11,16 @@
    - + @include('form.text', ['name' => 'display_name'])
    - + @include('form.text', ['name' => 'description'])
    +
    + @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ]) +
    @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2')
    @@ -34,15 +37,16 @@
    -
    @include('settings.roles.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])
    -
    @include('settings.roles.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])
    -
    @include('settings.roles.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])
    -
    @include('settings.roles.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])
    -
    @include('settings.roles.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])
    -
    @include('settings.roles.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])
    -
    @include('settings.roles.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])
    +
    @include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])

    {{ trans('settings.roles_system_warning') }}

    @@ -72,22 +76,22 @@ {{ trans('common.toggle_all') }}
    @@ -96,22 +100,22 @@ {{ trans('common.toggle_all') }} @@ -120,24 +124,24 @@ {{ trans('common.toggle_all') }} @@ -146,24 +150,24 @@ {{ trans('common.toggle_all') }} @@ -171,17 +175,17 @@
    {{ trans('entities.images') }}
    {{ trans('common.toggle_all') }} - + @@ -189,17 +193,17 @@
    {{ trans('entities.attachments') }}
    {{ trans('common.toggle_all') }} - + @@ -207,17 +211,17 @@
    {{ trans('entities.comments') }}
    {{ trans('common.toggle_all') }} - +
    {{ trans('settings.recycle_bin_deleted_item') }}{{ trans('settings.recycle_bin_deleted_item') }}{{ trans('settings.recycle_bin_deleted_parent') }} {{ trans('settings.recycle_bin_deleted_by') }} {{ trans('settings.recycle_bin_deleted_at') }}
    +

    {{ trans('settings.recycle_bin_contents_empty') }}

    @include('partials.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by]) + @if($deletion->deletable->getParent()) +
    + @icon($deletion->deletable->getParent()->getType()) +
    + {{ $deletion->deletable->getParent()->name }} +
    +
    + @endif +
    @include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by]) {{ $deletion->created_at }}
    id}") }}">{{ $role->display_name }}{{ $role->description }} + @if($role->mfa_enforced) + @icon('lock') + @endif + {{ $role->description }} + {{ $role->users->count() }}
    - @include('settings.roles.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')]) - @include('settings.roles.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')]) - @include('settings.roles.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
    @include('settings.roles.checkbox', ['permission' => 'image-create-all', 'label' => ''])@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => '']) {{ trans('settings.role_controlled_by_asset') }} - @include('settings.roles.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
    @include('settings.roles.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => '']) {{ trans('settings.role_controlled_by_asset') }} - @include('settings.roles.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
    @include('settings.roles.checkbox', ['permission' => 'comment-create-all', 'label' => ''])@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => '']) {{ trans('settings.role_controlled_by_asset') }} - @include('settings.roles.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
    - @include('settings.roles.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')]) + @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
    - @include('settings.roles.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')]) + @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
    @@ -236,7 +240,7 @@

    {{ trans('settings.role_users') }}

    - @if(isset($role) && count($role->users) > 0) + @if(count($role->users ?? []) > 0)
    @foreach($role->users as $user)
    diff --git a/resources/views/shelves/_breadcrumbs.blade.php b/resources/views/shelves/_breadcrumbs.blade.php deleted file mode 100644 index 91b4252ef..000000000 --- a/resources/views/shelves/_breadcrumbs.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/shelves/create.blade.php b/resources/views/shelves/create.blade.php index bea20eca9..95b459068 100644 --- a/resources/views/shelves/create.blade.php +++ b/resources/views/shelves/create.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ '/shelves' => [ 'text' => trans('entities.shelves'), 'icon' => 'bookshelf', @@ -20,7 +20,7 @@

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

    - @include('shelves.form', ['shelf' => null, 'books' => $books]) + @include('shelves.parts.form', ['shelf' => null, 'books' => $books])
    diff --git a/resources/views/shelves/delete.blade.php b/resources/views/shelves/delete.blade.php index 2a78227bd..42d1f5d84 100644 --- a/resources/views/shelves/delete.blade.php +++ b/resources/views/shelves/delete.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ $shelf, $shelf->getUrl('/delete') => [ 'text' => trans('entities.shelves_delete'), diff --git a/resources/views/shelves/edit.blade.php b/resources/views/shelves/edit.blade.php index 5ae3638fe..0114678eb 100644 --- a/resources/views/shelves/edit.blade.php +++ b/resources/views/shelves/edit.blade.php @@ -1,11 +1,11 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body')
    - @include('partials.breadcrumbs', ['crumbs' => [ + @include('entities.breadcrumbs', ['crumbs' => [ $shelf, $shelf->getUrl('/edit') => [ 'text' => trans('entities.shelves_edit'), @@ -18,7 +18,7 @@

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

    - @include('shelves.form', ['model' => $shelf]) + @include('shelves.parts.form', ['model' => $shelf])
    diff --git a/resources/views/shelves/index.blade.php b/resources/views/shelves/index.blade.php index 21c33aa9c..5c25356b0 100644 --- a/resources/views/shelves/index.blade.php +++ b/resources/views/shelves/index.blade.php @@ -1,7 +1,7 @@ -@extends('tri-layout') +@extends('layouts.tri') @section('body') - @include('shelves.list', ['shelves' => $shelves, 'view' => $view]) + @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view]) @stop @section('right') @@ -15,7 +15,7 @@ {{ trans('entities.shelves_new_action') }} @endif - @include('partials.view-toggle', ['view' => $view, 'type' => 'shelves']) + @include('entities.view-toggle', ['view' => $view, 'type' => 'shelves'])
    @@ -25,14 +25,14 @@ @if($recents)
    {{ trans('entities.recently_viewed') }}
    - @include('partials.entity-list', ['entities' => $recents, 'style' => 'compact']) + @include('entities.list', ['entities' => $recents, 'style' => 'compact'])
    @endif
    {{ $user->name }} id}") }}"> - {{ $user->name }}
    {{ $user->email }} + {{ $user->name }} +
    + {{ $user->email }} + @if($user->mfa_values_count > 0) + @icon('lock') + @endif
    diff --git a/resources/views/users/form.blade.php b/resources/views/users/parts/form.blade.php similarity index 98% rename from resources/views/users/form.blade.php rename to resources/views/users/parts/form.blade.php index 763c387d4..7105e2ff1 100644 --- a/resources/views/users/form.blade.php +++ b/resources/views/users/parts/form.blade.php @@ -56,7 +56,7 @@ {{ trans('settings.users_send_invite_text') }}

    - @include('components.toggle-switch', [ + @include('form.toggle-switch', [ 'name' => 'send_invite', 'value' => old('send_invite', 'true') === 'true', 'label' => trans('settings.users_send_invite_option') diff --git a/resources/views/users/profile.blade.php b/resources/views/users/profile.blade.php index 5a76a222a..b59c80ec6 100644 --- a/resources/views/users/profile.blade.php +++ b/resources/views/users/profile.blade.php @@ -1,4 +1,4 @@ -@extends('simple-layout') +@extends('layouts.simple') @section('body') @@ -9,7 +9,7 @@
    {{ trans('entities.recent_activity') }}
    - @include('partials.activity-list', ['activity' => $activity]) + @include('common.activity-list', ['activity' => $activity])
    @@ -64,7 +64,7 @@ @endif @if (count($recentlyCreated['pages']) > 0) - @include('partials.entity-list', ['entities' => $recentlyCreated['pages'], 'showPath' => true]) + @include('entities.list', ['entities' => $recentlyCreated['pages'], 'showPath' => true]) @else

    {{ trans('entities.profile_not_created_pages', ['userName' => $user->name]) }}

    @endif @@ -78,7 +78,7 @@ @endif @if (count($recentlyCreated['chapters']) > 0) - @include('partials.entity-list', ['entities' => $recentlyCreated['chapters'], 'showPath' => true]) + @include('entities.list', ['entities' => $recentlyCreated['chapters'], 'showPath' => true]) @else

    {{ trans('entities.profile_not_created_chapters', ['userName' => $user->name]) }}

    @endif @@ -92,7 +92,7 @@ @endif @if (count($recentlyCreated['books']) > 0) - @include('partials.entity-list', ['entities' => $recentlyCreated['books'], 'showPath' => true]) + @include('entities.list', ['entities' => $recentlyCreated['books'], 'showPath' => true]) @else

    {{ trans('entities.profile_not_created_books', ['userName' => $user->name]) }}

    @endif @@ -106,7 +106,7 @@ @endif @if (count($recentlyCreated['shelves']) > 0) - @include('partials.entity-list', ['entities' => $recentlyCreated['shelves'], 'showPath' => true]) + @include('entities.list', ['entities' => $recentlyCreated['shelves'], 'showPath' => true]) @else

    {{ trans('entities.profile_not_created_shelves', ['userName' => $user->name]) }}

    @endif diff --git a/routes/api.php b/routes/api.php index 44643d6d4..a6ed0c8f1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,9 +3,8 @@ /** * Routes for the BookStack API. * Routes have a uri prefix of /api/. - * Controllers are all within app/Http/Controllers/Api + * Controllers are all within app/Http/Controllers/Api. */ - Route::get('docs', 'ApiDocsController@display'); Route::get('docs.json', 'ApiDocsController@json'); @@ -18,6 +17,7 @@ Route::delete('books/{id}', 'BookApiController@delete'); Route::get('books/{id}/export/html', 'BookExportApiController@exportHtml'); Route::get('books/{id}/export/pdf', 'BookExportApiController@exportPdf'); Route::get('books/{id}/export/plaintext', 'BookExportApiController@exportPlainText'); +Route::get('books/{id}/export/markdown', 'BookExportApiController@exportMarkdown'); Route::get('chapters', 'ChapterApiController@list'); Route::post('chapters', 'ChapterApiController@create'); @@ -28,6 +28,7 @@ Route::delete('chapters/{id}', 'ChapterApiController@delete'); Route::get('chapters/{id}/export/html', 'ChapterExportApiController@exportHtml'); Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf'); Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText'); +Route::get('chapters/{id}/export/markdown', 'ChapterExportApiController@exportMarkdown'); Route::get('pages', 'PageApiController@list'); Route::post('pages', 'PageApiController@create'); @@ -38,6 +39,7 @@ Route::delete('pages/{id}', 'PageApiController@delete'); Route::get('pages/{id}/export/html', 'PageExportApiController@exportHtml'); Route::get('pages/{id}/export/pdf', 'PageExportApiController@exportPdf'); Route::get('pages/{id}/export/plaintext', 'PageExportApiController@exportPlainText'); +Route::get('pages/{id}/export/markdown', 'PageExportApiController@exportMarkDown'); Route::get('shelves', 'BookshelfApiController@list'); Route::post('shelves', 'BookshelfApiController@create'); diff --git a/routes/web.php b/routes/web.php index 72d089078..a823b73c8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,7 @@ 'auth'], function () { @@ -48,6 +48,8 @@ Route::group(['middleware' => 'auth'], function () { Route::put('/{bookSlug}/sort', 'BookSortController@update'); Route::get('/{bookSlug}/export/html', 'BookExportController@html'); Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf'); + Route::get('/{bookSlug}/export/markdown', 'BookExportController@markdown'); + Route::get('/{bookSlug}/export/zip', 'BookExportController@zip'); Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText'); // Pages @@ -58,6 +60,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageExportController@pdf'); Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageExportController@html'); + Route::get('/{bookSlug}/page/{pageSlug}/export/markdown', 'PageExportController@markdown'); Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageExportController@plainText'); Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove'); @@ -92,6 +95,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showPermissions'); Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterExportController@pdf'); Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterExportController@html'); + Route::get('/{bookSlug}/chapter/{chapterSlug}/export/markdown', 'ChapterExportController@markdown'); Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterExportController@plainText'); Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@permissions'); Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete'); @@ -167,7 +171,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/custom-head-content', 'HomeController@customHeadContent'); // Settings - Route::group(['prefix' => 'settings'], function() { + Route::group(['prefix' => 'settings'], function () { Route::get('/', 'SettingController@index')->name('settings'); Route::post('/', 'SettingController@update'); @@ -219,15 +223,27 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/roles/{id}', 'RoleController@edit'); Route::put('/roles/{id}', 'RoleController@update'); }); - }); +// MFA routes +Route::group(['middleware' => 'mfa-setup'], function () { + Route::get('/mfa/setup', 'Auth\MfaController@setup'); + Route::get('/mfa/totp/generate', 'Auth\MfaTotpController@generate'); + Route::post('/mfa/totp/confirm', 'Auth\MfaTotpController@confirm'); + Route::get('/mfa/backup_codes/generate', 'Auth\MfaBackupCodesController@generate'); + Route::post('/mfa/backup_codes/confirm', 'Auth\MfaBackupCodesController@confirm'); +}); +Route::group(['middleware' => 'guest'], function () { + Route::get('/mfa/verify', 'Auth\MfaController@verify'); + Route::post('/mfa/totp/verify', 'Auth\MfaTotpController@verify'); + Route::post('/mfa/backup_codes/verify', 'Auth\MfaBackupCodesController@verify'); +}); +Route::delete('/mfa/{method}/remove', 'Auth\MfaController@remove')->middleware('auth'); + // Social auth routes Route::get('/login/service/{socialDriver}', 'Auth\SocialController@login'); Route::get('/login/service/{socialDriver}/callback', 'Auth\SocialController@callback'); -Route::group(['middleware' => 'auth'], function () { - Route::post('/login/service/{socialDriver}/detach', 'Auth\SocialController@detach'); -}); +Route::post('/login/service/{socialDriver}/detach', 'Auth\SocialController@detach')->middleware('auth'); Route::get('/register/service/{socialDriver}', 'Auth\SocialController@register'); // Login/Logout routes @@ -260,4 +276,4 @@ Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm'); Route::post('/password/reset', 'Auth\ResetPasswordController@reset'); -Route::fallback('HomeController@getNotFound')->name('fallback'); \ No newline at end of file +Route::fallback('HomeController@notFound')->name('fallback'); diff --git a/server.php b/server.php index f65c7c444..de038652f 100644 --- a/server.php +++ b/server.php @@ -1,12 +1,10 @@ */ - $uri = urldecode( parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ); @@ -14,8 +12,8 @@ $uri = urldecode( // This file allows us to emulate Apache's "mod_rewrite" functionality from the // built-in PHP web server. This provides a convenient way to test a Laravel // application without having installed a "real" web server software here. -if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { +if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) { return false; } -require_once __DIR__.'/public/index.php'; +require_once __DIR__ . '/public/index.php'; diff --git a/tests/ActivityTrackingTest.php b/tests/ActivityTrackingTest.php index 9c3fe273c..494a1f506 100644 --- a/tests/ActivityTrackingTest.php +++ b/tests/ActivityTrackingTest.php @@ -1,11 +1,11 @@ -take(10); diff --git a/tests/Api/ApiAuthTest.php b/tests/Api/ApiAuthTest.php index 302093947..c45bd77ee 100644 --- a/tests/Api/ApiAuthTest.php +++ b/tests/Api/ApiAuthTest.php @@ -1,4 +1,6 @@ -get($this->endpoint); $resp->assertStatus(401); - $resp->assertJson($this->errorResponse("No authorization token found on the request", 401)); + $resp->assertJson($this->errorResponse('No authorization token found on the request', 401)); } public function test_bad_token_format_throws_error() { - $resp = $this->get($this->endpoint, ['Authorization' => "Token abc123"]); + $resp = $this->get($this->endpoint, ['Authorization' => 'Token abc123']); $resp->assertStatus(401); - $resp->assertJson($this->errorResponse("An authorization token was found on the request but the format appeared incorrect", 401)); + $resp->assertJson($this->errorResponse('An authorization token was found on the request but the format appeared incorrect', 401)); } public function test_token_with_non_existing_id_throws_error() { - $resp = $this->get($this->endpoint, ['Authorization' => "Token abc:123"]); + $resp = $this->get($this->endpoint, ['Authorization' => 'Token abc:123']); $resp->assertStatus(401); - $resp->assertJson($this->errorResponse("No matching API token was found for the provided authorization token", 401)); + $resp->assertJson($this->errorResponse('No matching API token was found for the provided authorization token', 401)); } public function test_token_with_bad_secret_value_throws_error() { $resp = $this->get($this->endpoint, ['Authorization' => "Token {$this->apiTokenId}:123"]); $resp->assertStatus(401); - $resp->assertJson($this->errorResponse("The secret provided for the given used API token is incorrect", 401)); + $resp->assertJson($this->errorResponse('The secret provided for the given used API token is incorrect', 401)); } public function test_api_access_permission_required_to_access_api() @@ -65,7 +67,7 @@ class ApiAuthTest extends TestCase $resp = $this->get($this->endpoint, $this->apiAuthHeader()); $resp->assertStatus(403); - $resp->assertJson($this->errorResponse("The owner of the used API token does not have permission to make API calls", 403)); + $resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403)); } public function test_api_access_permission_required_to_access_api_with_session_auth() @@ -86,7 +88,7 @@ class ApiAuthTest extends TestCase $this->actingAs($editor, 'standard'); $resp = $this->get($this->endpoint); $resp->assertStatus(403); - $resp->assertJson($this->errorResponse("The owner of the used API token does not have permission to make API calls", 403)); + $resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403)); } public function test_token_expiry_checked() @@ -102,7 +104,7 @@ class ApiAuthTest extends TestCase $token->save(); $resp = $this->get($this->endpoint, $this->apiAuthHeader()); - $resp->assertJson($this->errorResponse("The authorization token used has expired", 403)); + $resp->assertJson($this->errorResponse('The authorization token used has expired', 403)); } public function test_email_confirmation_checked_using_api_auth() @@ -116,7 +118,7 @@ class ApiAuthTest extends TestCase $resp = $this->get($this->endpoint, $this->apiAuthHeader()); $resp->assertStatus(401); - $resp->assertJson($this->errorResponse("The email address for the account in use needs to be confirmed", 401)); + $resp->assertJson($this->errorResponse('The email address for the account in use needs to be confirmed', 401)); } public function test_rate_limit_headers_active_on_requests() @@ -141,7 +143,7 @@ class ApiAuthTest extends TestCase $resp->assertJson([ 'error' => [ 'code' => 429, - ] + ], ]); } -} \ No newline at end of file +} diff --git a/tests/Api/ApiConfigTest.php b/tests/Api/ApiConfigTest.php index def62c94d..af808a76c 100644 --- a/tests/Api/ApiConfigTest.php +++ b/tests/Api/ApiConfigTest.php @@ -1,4 +1,6 @@ -actingAsApiEditor()->get($this->endpoint); $resp->assertHeader('x-ratelimit-limit', 10); } - -} \ No newline at end of file +} diff --git a/tests/Api/ApiDocsTest.php b/tests/Api/ApiDocsTest.php index 1687c64a1..90d107eb3 100644 --- a/tests/Api/ApiDocsTest.php +++ b/tests/Api/ApiDocsTest.php @@ -1,4 +1,6 @@ -assertStatus(200); $resp->assertHeader('Content-Type', 'application/json'); $resp->assertJson([ - 'docs' => [ [ + 'docs' => [[ 'name' => 'docs-display', - 'uri' => 'api/docs' - ] ] + 'uri' => 'api/docs', + ]], ]); } @@ -55,4 +57,4 @@ class ApiDocsTest extends TestCase $resp = $this->get('/api/docs'); $resp->assertStatus(200); } -} \ No newline at end of file +} diff --git a/tests/Api/ApiListingTest.php b/tests/Api/ApiListingTest.php index c3d9bc108..f90ec5a3d 100644 --- a/tests/Api/ApiListingTest.php +++ b/tests/Api/ApiListingTest.php @@ -1,4 +1,6 @@ -orderBy('id')->take(3)->get(); $resp = $this->get($this->endpoint . '?count=1'); - $resp->assertJsonMissing(['name' => $books[1]->name ]); + $resp->assertJsonMissing(['name' => $books[1]->name]); $resp = $this->get($this->endpoint . '?count=1&offset=1000'); $resp->assertJsonCount(0, 'data'); @@ -38,19 +40,19 @@ class ApiListingTest extends TestCase $this->actingAsApiEditor(); $sortChecks = [ - '-id' => Book::visible()->orderBy('id', 'desc')->first(), + '-id' => Book::visible()->orderBy('id', 'desc')->first(), '+name' => Book::visible()->orderBy('name', 'asc')->first(), - 'name' => Book::visible()->orderBy('name', 'asc')->first(), - '-name' => Book::visible()->orderBy('name', 'desc')->first() + 'name' => Book::visible()->orderBy('name', 'asc')->first(), + '-name' => Book::visible()->orderBy('name', 'desc')->first(), ]; foreach ($sortChecks as $sortOption => $result) { $resp = $this->get($this->endpoint . '?count=1&sort=' . $sortOption); $resp->assertJson(['data' => [ [ - 'id' => $result->id, + 'id' => $result->id, 'name' => $result->name, - ] + ], ]]); } } @@ -64,11 +66,11 @@ class ApiListingTest extends TestCase $filterChecks = [ // Test different types of filter - "filter[id]={$book->id}" => 1, - "filter[id:ne]={$book->id}" => Book::visible()->where('id', '!=', $book->id)->count(), - "filter[id:gt]={$book->id}" => Book::visible()->where('id', '>', $book->id)->count(), - "filter[id:gte]={$book->id}" => Book::visible()->where('id', '>=', $book->id)->count(), - "filter[id:lt]={$book->id}" => Book::visible()->where('id', '<', $book->id)->count(), + "filter[id]={$book->id}" => 1, + "filter[id:ne]={$book->id}" => Book::visible()->where('id', '!=', $book->id)->count(), + "filter[id:gt]={$book->id}" => Book::visible()->where('id', '>', $book->id)->count(), + "filter[id:gte]={$book->id}" => Book::visible()->where('id', '>=', $book->id)->count(), + "filter[id:lt]={$book->id}" => Book::visible()->where('id', '<', $book->id)->count(), "filter[name:like]={$encodedNameSubstr}%" => Book::visible()->where('name', 'like', $nameSubstr . '%')->count(), // Test mulitple filters 'and' together @@ -86,7 +88,7 @@ class ApiListingTest extends TestCase $this->actingAsApiEditor(); $bookCount = Book::query()->count(); $resp = $this->get($this->endpoint . '?count=1'); - $resp->assertJson(['total' => $bookCount ]); + $resp->assertJson(['total' => $bookCount]); } public function test_total_on_results_shows_correctly_when_offset_provided() @@ -94,7 +96,6 @@ class ApiListingTest extends TestCase $this->actingAsApiEditor(); $bookCount = Book::query()->count(); $resp = $this->get($this->endpoint . '?count=1&offset=1'); - $resp->assertJson(['total' => $bookCount ]); + $resp->assertJson(['total' => $bookCount]); } - -} \ No newline at end of file +} diff --git a/tests/Api/BooksApiTest.php b/tests/Api/BooksApiTest.php index a36acdd02..91e2db9e5 100644 --- a/tests/Api/BooksApiTest.php +++ b/tests/Api/BooksApiTest.php @@ -1,4 +1,6 @@ -getJson($this->baseEndpoint . '?count=1&sort=+id'); $resp->assertJson(['data' => [ [ - 'id' => $firstBook->id, + 'id' => $firstBook->id, 'name' => $firstBook->name, 'slug' => $firstBook->slug, - ] + ], ]]); } @@ -28,7 +30,7 @@ class BooksApiTest extends TestCase { $this->actingAsApiEditor(); $details = [ - 'name' => 'My API book', + 'name' => 'My API book', 'description' => 'A book created via the API', ]; @@ -49,12 +51,12 @@ class BooksApiTest extends TestCase $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(422); $resp->assertJson([ - "error" => [ - "message" => "The given data was invalid.", - "validation" => [ - "name" => ["The name field is required."] + 'error' => [ + 'message' => 'The given data was invalid.', + 'validation' => [ + 'name' => ['The name field is required.'], ], - "code" => 422, + 'code' => 422, ], ]); } @@ -68,8 +70,8 @@ class BooksApiTest extends TestCase $resp->assertStatus(200); $resp->assertJson([ - 'id' => $book->id, - 'slug' => $book->slug, + 'id' => $book->id, + 'slug' => $book->slug, 'created_by' => [ 'name' => $book->createdBy->name, ], @@ -77,7 +79,7 @@ class BooksApiTest extends TestCase 'name' => $book->createdBy->name, ], 'owned_by' => [ - 'name' => $book->ownedBy->name + 'name' => $book->ownedBy->name, ], ]); } @@ -87,7 +89,7 @@ class BooksApiTest extends TestCase $this->actingAsApiEditor(); $book = Book::visible()->first(); $details = [ - 'name' => 'My updated API book', + 'name' => 'My updated API book', 'description' => 'A book created via the API', ]; @@ -140,4 +142,30 @@ class BooksApiTest extends TestCase $resp->assertStatus(200); $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"'); } -} \ No newline at end of file + + public function test_export_markdown_endpoint() + { + $this->actingAsApiEditor(); + $book = Book::visible()->has('pages')->has('chapters')->first(); + + $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/markdown"); + $resp->assertStatus(200); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"'); + $resp->assertSee('# ' . $book->name); + $resp->assertSee('# ' . $book->pages()->first()->name); + $resp->assertSee('# ' . $book->chapters()->first()->name); + } + + public function test_cant_export_when_not_have_permission() + { + $types = ['html', 'plaintext', 'pdf', 'markdown']; + $this->actingAsApiEditor(); + $this->removePermissionFromUser($this->getEditor(), 'content-export'); + + $book = Book::visible()->first(); + foreach ($types as $type) { + $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}"); + $this->assertPermissionError($resp); + } + } +} diff --git a/tests/Api/ChaptersApiTest.php b/tests/Api/ChaptersApiTest.php index c7368eaee..c9ed1a289 100644 --- a/tests/Api/ChaptersApiTest.php +++ b/tests/Api/ChaptersApiTest.php @@ -1,4 +1,6 @@ -getJson($this->baseEndpoint . '?count=1&sort=+id'); $resp->assertJson(['data' => [ [ - 'id' => $firstChapter->id, - 'name' => $firstChapter->name, - 'slug' => $firstChapter->slug, - 'book_id' => $firstChapter->book->id, + 'id' => $firstChapter->id, + 'name' => $firstChapter->name, + 'slug' => $firstChapter->slug, + 'book_id' => $firstChapter->book->id, 'priority' => $firstChapter->priority, - ] + ], ]]); } @@ -32,15 +34,15 @@ class ChaptersApiTest extends TestCase $this->actingAsApiEditor(); $book = Book::query()->first(); $details = [ - 'name' => 'My API chapter', + 'name' => 'My API chapter', 'description' => 'A chapter created via the API', - 'book_id' => $book->id, - 'tags' => [ + 'book_id' => $book->id, + 'tags' => [ [ - 'name' => 'tagname', + 'name' => 'tagname', 'value' => 'tagvalue', - ] - ] + ], + ], ]; $resp = $this->postJson($this->baseEndpoint, $details); @@ -48,10 +50,10 @@ class ChaptersApiTest extends TestCase $newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first(); $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug])); $this->assertDatabaseHas('tags', [ - 'entity_id' => $newItem->id, + 'entity_id' => $newItem->id, 'entity_type' => $newItem->getMorphClass(), - 'name' => 'tagname', - 'value' => 'tagvalue', + 'name' => 'tagname', + 'value' => 'tagvalue', ]); $resp->assertJsonMissing(['pages' => []]); $this->assertActivityExists('chapter_create', $newItem); @@ -62,14 +64,14 @@ class ChaptersApiTest extends TestCase $this->actingAsApiEditor(); $book = Book::query()->first(); $details = [ - 'book_id' => $book->id, + 'book_id' => $book->id, 'description' => 'A chapter created via the API', ]; $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(422); $resp->assertJson($this->validationResponse([ - "name" => ["The name field is required."] + 'name' => ['The name field is required.'], ])); } @@ -77,14 +79,14 @@ class ChaptersApiTest extends TestCase { $this->actingAsApiEditor(); $details = [ - 'name' => 'My api chapter', + 'name' => 'My api chapter', 'description' => 'A chapter created via the API', ]; $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(422); $resp->assertJson($this->validationResponse([ - "book_id" => ["The book id field is required."] + 'book_id' => ['The book id field is required.'], ])); } @@ -97,24 +99,24 @@ class ChaptersApiTest extends TestCase $resp = $this->getJson($this->baseEndpoint . "/{$chapter->id}"); $resp->assertStatus(200); $resp->assertJson([ - 'id' => $chapter->id, - 'slug' => $chapter->slug, + 'id' => $chapter->id, + 'slug' => $chapter->slug, 'created_by' => [ 'name' => $chapter->createdBy->name, ], - 'book_id' => $chapter->book_id, + 'book_id' => $chapter->book_id, 'updated_by' => [ 'name' => $chapter->createdBy->name, ], 'owned_by' => [ - 'name' => $chapter->ownedBy->name + 'name' => $chapter->ownedBy->name, ], 'pages' => [ [ - 'id' => $page->id, + 'id' => $page->id, 'slug' => $page->slug, 'name' => $page->name, - ] + ], ], ]); $resp->assertJsonCount($chapter->pages()->count(), 'pages'); @@ -125,13 +127,13 @@ class ChaptersApiTest extends TestCase $this->actingAsApiEditor(); $chapter = Chapter::visible()->first(); $details = [ - 'name' => 'My updated API chapter', + 'name' => 'My updated API chapter', 'description' => 'A chapter created via the API', - 'tags' => [ + 'tags' => [ [ - 'name' => 'freshtag', + 'name' => 'freshtag', 'value' => 'freshtagval', - ] + ], ], ]; @@ -140,7 +142,7 @@ class ChaptersApiTest extends TestCase $resp->assertStatus(200); $resp->assertJson(array_merge($details, [ - 'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id + 'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id, ])); $this->assertActivityExists('chapter_update', $chapter); } @@ -186,4 +188,29 @@ class ChaptersApiTest extends TestCase $resp->assertStatus(200); $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"'); } -} \ No newline at end of file + + public function test_export_markdown_endpoint() + { + $this->actingAsApiEditor(); + $chapter = Chapter::visible()->has('pages')->first(); + + $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/markdown"); + $resp->assertStatus(200); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.md"'); + $resp->assertSee('# ' . $chapter->name); + $resp->assertSee('# ' . $chapter->pages()->first()->name); + } + + public function test_cant_export_when_not_have_permission() + { + $types = ['html', 'plaintext', 'pdf', 'markdown']; + $this->actingAsApiEditor(); + $this->removePermissionFromUser($this->getEditor(), 'content-export'); + + $chapter = Chapter::visible()->has('pages')->first(); + foreach ($types as $type) { + $resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/{$type}"); + $this->assertPermissionError($resp); + } + } +} diff --git a/tests/Api/PagesApiTest.php b/tests/Api/PagesApiTest.php index e08e9b1b7..4eb109d9d 100644 --- a/tests/Api/PagesApiTest.php +++ b/tests/Api/PagesApiTest.php @@ -1,4 +1,6 @@ -getJson($this->baseEndpoint . '?count=1&sort=+id'); $resp->assertJson(['data' => [ [ - 'id' => $firstPage->id, - 'name' => $firstPage->name, - 'slug' => $firstPage->slug, - 'book_id' => $firstPage->book->id, + 'id' => $firstPage->id, + 'name' => $firstPage->name, + 'slug' => $firstPage->slug, + 'book_id' => $firstPage->book->id, 'priority' => $firstPage->priority, - ] + ], ]]); } @@ -33,15 +35,15 @@ class PagesApiTest extends TestCase $this->actingAsApiEditor(); $book = Book::query()->first(); $details = [ - 'name' => 'My API page', + 'name' => 'My API page', 'book_id' => $book->id, - 'html' => '

    My new page content

    ', - 'tags' => [ + 'html' => '

    My new page content

    ', + 'tags' => [ [ - 'name' => 'tagname', + 'name' => 'tagname', 'value' => 'tagvalue', - ] - ] + ], + ], ]; $resp = $this->postJson($this->baseEndpoint, $details); @@ -50,10 +52,10 @@ class PagesApiTest extends TestCase $newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first(); $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug])); $this->assertDatabaseHas('tags', [ - 'entity_id' => $newItem->id, + 'entity_id' => $newItem->id, 'entity_type' => $newItem->getMorphClass(), - 'name' => 'tagname', - 'value' => 'tagvalue', + 'name' => 'tagname', + 'value' => 'tagvalue', ]); $resp->assertSeeText('My new page content'); $resp->assertJsonMissing(['book' => []]); @@ -66,13 +68,13 @@ class PagesApiTest extends TestCase $book = Book::query()->first(); $details = [ 'book_id' => $book->id, - 'html' => '

    A page created via the API

    ', + 'html' => '

    A page created via the API

    ', ]; $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(422); $resp->assertJson($this->validationResponse([ - "name" => ["The name field is required."] + 'name' => ['The name field is required.'], ])); } @@ -87,8 +89,8 @@ class PagesApiTest extends TestCase $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(422); $resp->assertJson($this->validationResponse([ - "book_id" => ["The book id field is required when chapter id is not present."], - "chapter_id" => ["The chapter id field is required when book id is not present."] + 'book_id' => ['The book id field is required when chapter id is not present.'], + 'chapter_id' => ['The chapter id field is required when book id is not present.'], ])); $chapter = Chapter::visible()->first(); @@ -105,8 +107,8 @@ class PagesApiTest extends TestCase $this->actingAsApiEditor(); $book = Book::visible()->first(); $details = [ - 'book_id' => $book->id, - 'name' => 'My api page', + 'book_id' => $book->id, + 'name' => 'My api page', 'markdown' => "# A new API page \n[link](https://example.com)", ]; @@ -127,17 +129,17 @@ class PagesApiTest extends TestCase $resp = $this->getJson($this->baseEndpoint . "/{$page->id}"); $resp->assertStatus(200); $resp->assertJson([ - 'id' => $page->id, - 'slug' => $page->slug, + 'id' => $page->id, + 'slug' => $page->slug, 'created_by' => [ 'name' => $page->createdBy->name, ], - 'book_id' => $page->book_id, + 'book_id' => $page->book_id, 'updated_by' => [ 'name' => $page->createdBy->name, ], 'owned_by' => [ - 'name' => $page->ownedBy->name + 'name' => $page->ownedBy->name, ], ]); } @@ -165,9 +167,9 @@ class PagesApiTest extends TestCase 'html' => '

    A page created via the API

    ', 'tags' => [ [ - 'name' => 'freshtag', + 'name' => 'freshtag', 'value' => 'freshtagval', - ] + ], ], ]; @@ -177,7 +179,7 @@ class PagesApiTest extends TestCase $resp->assertStatus(200); unset($details['html']); $resp->assertJson(array_merge($details, [ - 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id + 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id, ])); $this->assertActivityExists('page_update', $page); } @@ -188,16 +190,16 @@ class PagesApiTest extends TestCase $page = Page::visible()->first(); $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); $details = [ - 'name' => 'My updated API page', + 'name' => 'My updated API page', 'chapter_id' => $chapter->id, - 'html' => '

    A page created via the API

    ', + 'html' => '

    A page created via the API

    ', ]; $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details); $resp->assertStatus(200); $resp->assertJson([ 'chapter_id' => $chapter->id, - 'book_id' => $chapter->book_id, + 'book_id' => $chapter->book_id, ]); } @@ -208,15 +210,36 @@ class PagesApiTest extends TestCase $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first(); $this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]); $details = [ - 'name' => 'My updated API page', + 'name' => 'My updated API page', 'chapter_id' => $chapter->id, - 'html' => '

    A page created via the API

    ', + 'html' => '

    A page created via the API

    ', ]; $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details); $resp->assertStatus(403); } + public function test_update_endpoint_does_not_wipe_content_if_no_html_or_md_provided() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + $originalContent = $page->html; + $details = [ + 'name' => 'My updated API page', + 'tags' => [ + [ + 'name' => 'freshtag', + 'value' => 'freshtagval', + ], + ], + ]; + + $this->putJson($this->baseEndpoint . "/{$page->id}", $details); + $page->refresh(); + + $this->assertEquals($originalContent, $page->html); + } + public function test_delete_endpoint() { $this->actingAsApiEditor(); @@ -258,4 +281,28 @@ class PagesApiTest extends TestCase $resp->assertStatus(200); $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"'); } -} \ No newline at end of file + + public function test_export_markdown_endpoint() + { + $this->actingAsApiEditor(); + $page = Page::visible()->first(); + + $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/markdown"); + $resp->assertStatus(200); + $resp->assertSee('# ' . $page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"'); + } + + public function test_cant_export_when_not_have_permission() + { + $types = ['html', 'plaintext', 'pdf', 'markdown']; + $this->actingAsApiEditor(); + $this->removePermissionFromUser($this->getEditor(), 'content-export'); + + $page = Page::visible()->first(); + foreach ($types as $type) { + $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/{$type}"); + $this->assertPermissionError($resp); + } + } +} diff --git a/tests/Api/ShelvesApiTest.php b/tests/Api/ShelvesApiTest.php index 32715dd0a..8868c686e 100644 --- a/tests/Api/ShelvesApiTest.php +++ b/tests/Api/ShelvesApiTest.php @@ -1,4 +1,6 @@ -getJson($this->baseEndpoint . '?count=1&sort=+id'); $resp->assertJson(['data' => [ [ - 'id' => $firstBookshelf->id, + 'id' => $firstBookshelf->id, 'name' => $firstBookshelf->name, 'slug' => $firstBookshelf->slug, - ] + ], ]]); } @@ -31,7 +33,7 @@ class ShelvesApiTest extends TestCase $books = Book::query()->take(2)->get(); $details = [ - 'name' => 'My API shelf', + 'name' => 'My API shelf', 'description' => 'A shelf created via the API', ]; @@ -43,8 +45,8 @@ class ShelvesApiTest extends TestCase foreach ($books as $index => $book) { $this->assertDatabaseHas('bookshelves_books', [ 'bookshelf_id' => $newItem->id, - 'book_id' => $book->id, - 'order' => $index, + 'book_id' => $book->id, + 'order' => $index, ]); } } @@ -59,12 +61,12 @@ class ShelvesApiTest extends TestCase $resp = $this->postJson($this->baseEndpoint, $details); $resp->assertStatus(422); $resp->assertJson([ - "error" => [ - "message" => "The given data was invalid.", - "validation" => [ - "name" => ["The name field is required."] + 'error' => [ + 'message' => 'The given data was invalid.', + 'validation' => [ + 'name' => ['The name field is required.'], ], - "code" => 422, + 'code' => 422, ], ]); } @@ -78,8 +80,8 @@ class ShelvesApiTest extends TestCase $resp->assertStatus(200); $resp->assertJson([ - 'id' => $shelf->id, - 'slug' => $shelf->slug, + 'id' => $shelf->id, + 'slug' => $shelf->slug, 'created_by' => [ 'name' => $shelf->createdBy->name, ], @@ -87,7 +89,7 @@ class ShelvesApiTest extends TestCase 'name' => $shelf->createdBy->name, ], 'owned_by' => [ - 'name' => $shelf->ownedBy->name + 'name' => $shelf->ownedBy->name, ], ]); } @@ -97,7 +99,7 @@ class ShelvesApiTest extends TestCase $this->actingAsApiEditor(); $shelf = Bookshelf::visible()->first(); $details = [ - 'name' => 'My updated API shelf', + 'name' => 'My updated API shelf', 'description' => 'A shelf created via the API', ]; @@ -136,4 +138,4 @@ class ShelvesApiTest extends TestCase $resp->assertStatus(204); $this->assertActivityExists('bookshelf_delete'); } -} \ No newline at end of file +} diff --git a/tests/Api/TestsApi.php b/tests/Api/TestsApi.php index 1ad4d14b6..683ca0c74 100644 --- a/tests/Api/TestsApi.php +++ b/tests/Api/TestsApi.php @@ -1,8 +1,9 @@ -actingAs($this->getEditor(), 'api'); + return $this; } @@ -20,7 +22,7 @@ trait TestsApi */ protected function errorResponse(string $message, int $code): array { - return ["error" => ["code" => $code, "message" => $message]]; + return ['error' => ['code' => $code, 'message' => $message]]; } /** @@ -29,18 +31,19 @@ trait TestsApi */ protected function validationResponse(array $messages): array { - $err = $this->errorResponse("The given data was invalid.", 422); + $err = $this->errorResponse('The given data was invalid.', 422); $err['error']['validation'] = $messages; + return $err; } + /** * Get an approved API auth header. */ protected function apiAuthHeader(): array { return [ - "Authorization" => "Token {$this->apiTokenId}:{$this->apiTokenSecret}" + 'Authorization' => "Token {$this->apiTokenId}:{$this->apiTokenSecret}", ]; } - -} \ No newline at end of file +} diff --git a/tests/AuditLogTest.php b/tests/AuditLogTest.php index 55a458786..bc36a184d 100644 --- a/tests/AuditLogTest.php +++ b/tests/AuditLogTest.php @@ -1,18 +1,20 @@ -actingAs( $this->getAdmin()); + $this->actingAs($this->getAdmin()); $page = Page::query()->first(); $pageName = $page->name; $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE); @@ -137,7 +139,5 @@ class AuditLogTest extends TestCase $resp = $this->actingAs($admin)->get('settings/audit?user=' . $editor->id); $resp->assertSeeText($chapter->name); $resp->assertDontSeeText($page->name); - } - -} \ No newline at end of file +} diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index f88fc1904..2380aad7b 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -1,5 +1,8 @@ -visit('/') @@ -130,15 +132,16 @@ class AuthTest extends BrowserKitTest ->seePageIs('/register/confirm/awaiting') ->see('Resend') ->visit('/books') - ->seePageIs('/register/confirm/awaiting') + ->seePageIs('/login') + ->visit('/register/confirm/awaiting') ->press('Resend Confirmation Email'); // Get confirmation and confirm notification matches $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); - Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) { + Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) { return $notification->token === $emailConfirmation->token; }); - + // Check confirmation email confirmation activation. $this->visit('/register/confirm/' . $emailConfirmation->token) ->seePageIs('/') @@ -171,10 +174,7 @@ class AuthTest extends BrowserKitTest ->seePageIs('/register/confirm') ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - $this->visit('/') - ->seePageIs('/register/confirm/awaiting'); - - auth()->logout(); + $this->assertNull(auth()->user()); $this->visit('/')->seePageIs('/login') ->type($user->email, '#email') @@ -208,10 +208,8 @@ class AuthTest extends BrowserKitTest ->seePageIs('/register/confirm') ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); - $this->visit('/') - ->seePageIs('/register/confirm/awaiting'); + $this->assertNull(auth()->user()); - auth()->logout(); $this->visit('/')->seePageIs('/login') ->type($user->email, '#email') ->type($user->password, '#password') @@ -278,8 +276,8 @@ class AuthTest extends BrowserKitTest ->press('Save') ->seePageIs('/settings/users'); - $userPassword = User::find($user->id)->password; - $this->assertTrue(Hash::check('newpassword', $userPassword)); + $userPassword = User::find($user->id)->password; + $this->assertTrue(Hash::check('newpassword', $userPassword)); } public function test_user_deletion() @@ -329,6 +327,18 @@ class AuthTest extends BrowserKitTest ->seePageIs('/login'); } + public function test_mfa_session_cleared_on_logout() + { + $user = $this->getEditor(); + $mfaSession = $this->app->make(MfaSession::class); + + $mfaSession->markVerifiedForUser($user); + $this->assertTrue($mfaSession->isVerifiedForUser($user)); + + $this->asAdmin()->visit('/logout'); + $this->assertFalse($mfaSession->isVerifiedForUser($user)); + } + public function test_reset_password_flow() { Notification::fake(); @@ -340,7 +350,7 @@ class AuthTest extends BrowserKitTest ->see('A password reset link will be sent to admin@admin.com if that email address is found in the system.'); $this->seeInDatabase('password_resets', [ - 'email' => 'admin@admin.com' + 'email' => 'admin@admin.com', ]); $user = User::where('email', '=', 'admin@admin.com')->first(); @@ -351,9 +361,9 @@ class AuthTest extends BrowserKitTest $this->visit('/password/reset/' . $n->first()->token) ->see('Reset Password') ->submitForm('Reset Password', [ - 'email' => 'admin@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass' + 'email' => 'admin@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', ])->seePageIs('/') ->see('Your password has been successfully reset'); } @@ -367,13 +377,12 @@ class AuthTest extends BrowserKitTest ->see('A password reset link will be sent to barry@admin.com if that email address is found in the system.') ->dontSee('We can\'t find a user'); - $this->visit('/password/reset/arandometokenvalue') ->see('Reset Password') ->submitForm('Reset Password', [ - 'email' => 'barry@admin.com', - 'password' => 'randompass', - 'password_confirmation' => 'randompass' + 'email' => 'barry@admin.com', + 'password' => 'randompass', + 'password_confirmation' => 'randompass', ])->followRedirects() ->seePageIs('/password/reset/arandometokenvalue') ->dontSee('We can\'t find a user') @@ -410,6 +419,14 @@ class AuthTest extends BrowserKitTest $login->assertRedirectedTo('http://localhost'); } + public function test_login_intended_redirect_does_not_factor_mfa_routes() + { + $this->get('/books')->assertRedirectedTo('/login'); + $this->get('/mfa/setup')->assertRedirectedTo('/login'); + $login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); + $login->assertRedirectedTo('/books'); + } + public function test_login_authenticates_admins_on_all_guards() { $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); @@ -442,8 +459,24 @@ class AuthTest extends BrowserKitTest $this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com')); } + public function test_logged_in_user_with_unconfirmed_email_is_logged_out() + { + $this->setSettings(['registration-confirmation' => 'true']); + $user = $this->getEditor(); + $user->email_confirmed = false; + $user->save(); + + auth()->login($user); + $this->assertTrue(auth()->check()); + + $this->get('/books'); + $this->assertRedirectedTo('/'); + + $this->assertFalse(auth()->check()); + } + /** - * Perform a login + * Perform a login. */ protected function login(string $email, string $password): AuthTest { diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 60859e324..9e0729a8e 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -1,8 +1,10 @@ -set([ - 'auth.method' => 'ldap', - 'auth.defaults.guard' => 'ldap', - 'services.ldap.base_dn' => 'dc=ldap,dc=local', - 'services.ldap.email_attribute' => 'mail', + 'auth.method' => 'ldap', + 'auth.defaults.guard' => 'ldap', + 'services.ldap.base_dn' => 'dc=ldap,dc=local', + 'services.ldap.email_attribute' => 'mail', 'services.ldap.display_name_attribute' => 'cn', - 'services.ldap.id_attribute' => 'uid', - 'services.ldap.user_to_groups' => false, - 'services.ldap.version' => '3', - 'services.ldap.user_filter' => '(&(uid=${user}))', - 'services.ldap.follow_referrals' => false, - 'services.ldap.tls_insecure' => false, - 'services.ldap.thumbnail_attribute' => null, + 'services.ldap.id_attribute' => 'uid', + 'services.ldap.user_to_groups' => false, + 'services.ldap.version' => '3', + 'services.ldap.user_filter' => '(&(uid=${user}))', + 'services.ldap.follow_referrals' => false, + 'services.ldap.tls_insecure' => false, + 'services.ldap.thumbnail_attribute' => null, ]); $this->mockLdap = \Mockery::mock(Ldap::class); $this->app[Ldap::class] = $this->mockLdap; @@ -91,8 +95,8 @@ class LdapTest extends TestCase ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $resp = $this->mockUserLogin(); @@ -105,16 +109,16 @@ class LdapTest extends TestCase $resp->assertElementExists('#home-default'); $resp->assertSee($this->mockUser->name); $this->assertDatabaseHas('users', [ - 'email' => $this->mockUser->email, - 'email_confirmed' => false, - 'external_auth_id' => $this->mockUser->name + 'email' => $this->mockUser->email, + 'email_confirmed' => false, + 'external_auth_id' => $this->mockUser->name, ]); } public function test_email_domain_restriction_active_on_new_ldap_login() { $this->setSettings([ - 'registration-restrict' => 'testing.com' + 'registration-restrict' => 'testing.com', ]); $this->commonLdapMocks(1, 1, 2, 4, 2); @@ -122,15 +126,14 @@ class LdapTest extends TestCase ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $resp = $this->mockUserLogin(); $resp->assertRedirect('/login'); $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.'); - $email = 'tester@invaliddomain.com'; $resp = $this->mockUserLogin($email); $resp->assertRedirect('/login'); @@ -147,9 +150,9 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'cn' => [$this->mockUser->name], - 'dn' => $ldapDn, - 'mail' => [$this->mockUser->email] + 'cn' => [$this->mockUser->name], + 'dn' => $ldapDn, + 'mail' => [$this->mockUser->email], ]]); $resp = $this->mockUserLogin(); @@ -167,13 +170,12 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'cn' => [$this->mockUser->name], - 'dn' => $ldapDn, + 'cn' => [$this->mockUser->name], + 'dn' => $ldapDn, 'my_custom_id' => ['cooluser456'], - 'mail' => [$this->mockUser->email] + 'mail' => [$this->mockUser->email], ]]); - $resp = $this->mockUserLogin(); $resp->assertRedirect('/'); $this->followRedirects($resp)->assertSee($this->mockUser->name); @@ -187,8 +189,8 @@ class LdapTest extends TestCase ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false); @@ -217,14 +219,14 @@ class LdapTest extends TestCase $userForm->assertDontSee('Password'); $save = $this->post('/settings/users/create', [ - 'name' => $this->mockUser->name, + 'name' => $this->mockUser->name, 'email' => $this->mockUser->email, ]); $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']); $save = $this->post('/settings/users/create', [ - 'name' => $this->mockUser->name, - 'email' => $this->mockUser->email, + 'name' => $this->mockUser->name, + 'email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, ]); $save->assertRedirect('/settings/users'); @@ -239,8 +241,8 @@ class LdapTest extends TestCase $editPage->assertDontSee('Password'); $update = $this->put("/settings/users/{$editUser->id}", [ - 'name' => $editUser->name, - 'email' => $editUser->email, + 'name' => $editUser->name, + 'email' => $editUser->email, 'external_auth_id' => 'test_auth_id', ]); $update->assertRedirect('/settings/users'); @@ -269,8 +271,8 @@ class LdapTest extends TestCase $this->mockUser->attachRole($existingRole); app('config')->set([ - 'services.ldap.user_to_groups' => true, - 'services.ldap.group_attribute' => 'memberOf', + 'services.ldap.user_to_groups' => true, + 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => false, ]); @@ -278,15 +280,15 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], - 'mail' => [$this->mockUser->email], + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => [$this->mockUser->email], 'memberof' => [ 'count' => 2, - 0 => "cn=ldaptester,ou=groups,dc=example,dc=com", - 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com", - ] + 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com', + 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com', + ], ]]); $this->mockUserLogin()->assertRedirect('/'); @@ -294,15 +296,15 @@ class LdapTest extends TestCase $user = User::where('email', $this->mockUser->email)->first(); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive->id + 'role_id' => $roleToReceive->id, ]); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive2->id + 'role_id' => $roleToReceive2->id, ]); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $existingRole->id + 'role_id' => $existingRole->id, ]); } @@ -314,8 +316,8 @@ class LdapTest extends TestCase $this->mockUser->attachRole($existingRole); app('config')->set([ - 'services.ldap.user_to_groups' => true, - 'services.ldap.group_attribute' => 'memberOf', + 'services.ldap.user_to_groups' => true, + 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); @@ -323,14 +325,14 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], - 'mail' => [$this->mockUser->email], + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => [$this->mockUser->email], 'memberof' => [ 'count' => 1, - 0 => "cn=ldaptester,ou=groups,dc=example,dc=com", - ] + 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com', + ], ]]); $this->mockUserLogin()->assertRedirect('/'); @@ -338,11 +340,11 @@ class LdapTest extends TestCase $user = User::query()->where('email', $this->mockUser->email)->first(); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive->id + 'role_id' => $roleToReceive->id, ]); $this->assertDatabaseMissing('role_user', [ 'user_id' => $user->id, - 'role_id' => $existingRole->id + 'role_id' => $existingRole->id, ]); } @@ -359,8 +361,8 @@ class LdapTest extends TestCase $roleToNotReceive = factory(Role::class)->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']); app('config')->set([ - 'services.ldap.user_to_groups' => true, - 'services.ldap.group_attribute' => 'memberOf', + 'services.ldap.user_to_groups' => true, + 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); @@ -368,14 +370,14 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], - 'mail' => [$this->mockUser->email], + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => [$this->mockUser->email], 'memberof' => [ 'count' => 1, - 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com", - ] + 0 => 'cn=ex-auth-a,ou=groups,dc=example,dc=com', + ], ]]); $this->mockUserLogin()->assertRedirect('/'); @@ -383,11 +385,11 @@ class LdapTest extends TestCase $user = User::query()->where('email', $this->mockUser->email)->first(); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive->id + 'role_id' => $roleToReceive->id, ]); $this->assertDatabaseMissing('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToNotReceive->id + 'role_id' => $roleToNotReceive->id, ]); } @@ -400,8 +402,8 @@ class LdapTest extends TestCase setting()->put('registration-role', $roleToReceive->id); app('config')->set([ - 'services.ldap.user_to_groups' => true, - 'services.ldap.group_attribute' => 'memberOf', + 'services.ldap.user_to_groups' => true, + 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); @@ -409,15 +411,15 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], - 'mail' => [$this->mockUser->email], + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => [$this->mockUser->email], 'memberof' => [ 'count' => 2, - 0 => "cn=ldaptester,ou=groups,dc=example,dc=com", - 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com", - ] + 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com', + 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com', + ], ]]); $this->mockUserLogin()->assertRedirect('/'); @@ -425,28 +427,28 @@ class LdapTest extends TestCase $user = User::query()->where('email', $this->mockUser->email)->first(); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive->id + 'role_id' => $roleToReceive->id, ]); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive2->id + 'role_id' => $roleToReceive2->id, ]); } public function test_login_uses_specified_display_name_attribute() { app('config')->set([ - 'services.ldap.display_name_attribute' => 'displayName' + 'services.ldap.display_name_attribute' => 'displayName', ]); $this->commonLdapMocks(1, 1, 2, 4, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], - 'displayname' => 'displayNameAttribute' + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'displayname' => 'displayNameAttribute', ]]); $this->mockUserLogin()->assertRedirect('/login'); @@ -461,7 +463,7 @@ class LdapTest extends TestCase public function test_login_uses_default_display_name_attribute_if_specified_not_present() { app('config')->set([ - 'services.ldap.display_name_attribute' => 'displayName' + 'services.ldap.display_name_attribute' => 'displayName', ]); $this->commonLdapMocks(1, 1, 2, 4, 2); @@ -469,8 +471,8 @@ class LdapTest extends TestCase ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $this->mockUserLogin()->assertRedirect('/login'); @@ -480,25 +482,25 @@ class LdapTest extends TestCase $resp->assertRedirect('/'); $this->get('/')->assertSee($this->mockUser->name); $this->assertDatabaseHas('users', [ - 'email' => $this->mockUser->email, - 'email_confirmed' => false, + 'email' => $this->mockUser->email, + 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, - 'name' => $this->mockUser->name + 'name' => $this->mockUser->name, ]); } protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort) { app('config')->set([ - 'services.ldap.server' => $serverString + 'services.ldap.server' => $serverString, ]); // Standard mocks $this->commonLdapMocks(0, 1, 1, 2, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $this->mockLdap->shouldReceive('connect')->once() @@ -564,8 +566,8 @@ class LdapTest extends TestCase ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $resp = $this->post('/login', [ @@ -573,7 +575,7 @@ class LdapTest extends TestCase 'password' => $this->mockUser->password, ]); $resp->assertJsonStructure([ - 'details_from_ldap' => [], + 'details_from_ldap' => [], 'details_bookstack_parsed' => [], ]); } @@ -603,8 +605,8 @@ class LdapTest extends TestCase ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn']) ->andReturn(['count' => 1, 0 => [ 'uid' => [hex2bin('FFF8F7')], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')] + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], ]]); $details = $ldapService->getUserDetails('test'); @@ -617,14 +619,14 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$this->mockUser->name], - 'cn' => [$this->mockUser->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], 'mail' => 'tester@example.com', ]], ['count' => 1, 0 => [ - 'uid' => ['Barry'], - 'cn' => ['Scott'], - 'dn' => ['dc=bscott' . config('services.ldap.base_dn')], + 'uid' => ['Barry'], + 'cn' => ['Scott'], + 'dn' => ['dc=bscott' . config('services.ldap.base_dn')], 'mail' => 'tester@example.com', ]]); @@ -644,39 +646,45 @@ class LdapTest extends TestCase setting()->put('registration-confirmation', 'true'); app('config')->set([ - 'services.ldap.user_to_groups' => true, - 'services.ldap.group_attribute' => 'memberOf', + 'services.ldap.user_to_groups' => true, + 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); - $this->commonLdapMocks(1, 1, 3, 4, 3, 2); + $this->commonLdapMocks(1, 1, 6, 8, 6, 4); $this->mockLdap->shouldReceive('searchAndGetEntries') - ->times(3) + ->times(6) ->andReturn(['count' => 1, 0 => [ - 'uid' => [$user->name], - 'cn' => [$user->name], - 'dn' => ['dc=test' . config('services.ldap.base_dn')], - 'mail' => [$user->email], + 'uid' => [$user->name], + 'cn' => [$user->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => [$user->email], 'memberof' => [ 'count' => 1, - 0 => "cn=ldaptester,ou=groups,dc=example,dc=com", - ] + 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com', + ], ]]); - $this->followingRedirects()->mockUserLogin()->assertSee('Thanks for registering!'); + $login = $this->followingRedirects()->mockUserLogin(); + $login->assertSee('Thanks for registering!'); $this->assertDatabaseHas('users', [ - 'email' => $user->email, + 'email' => $user->email, 'email_confirmed' => false, ]); $user = User::query()->where('email', '=', $user->email)->first(); $this->assertDatabaseHas('role_user', [ 'user_id' => $user->id, - 'role_id' => $roleToReceive->id + 'role_id' => $roleToReceive->id, ]); + $this->assertNull(auth()->user()); + $homePage = $this->get('/'); - $homePage->assertRedirect('/register/confirm/awaiting'); + $homePage->assertRedirect('/login'); + + $login = $this->followingRedirects()->mockUserLogin(); + $login->assertSee('Email Address Not Confirmed'); } public function test_failed_logins_are_logged_when_message_configured() @@ -696,11 +704,11 @@ class LdapTest extends TestCase $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ - 'cn' => [$this->mockUser->name], - 'dn' => $ldapDn, + 'cn' => [$this->mockUser->name], + 'dn' => $ldapDn, 'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')], - 'mail' => [$this->mockUser->email] + 'mail' => [$this->mockUser->email], ]]); $this->mockUserLogin() diff --git a/tests/Auth/MfaConfigurationTest.php b/tests/Auth/MfaConfigurationTest.php new file mode 100644 index 000000000..eb0e2faf0 --- /dev/null +++ b/tests/Auth/MfaConfigurationTest.php @@ -0,0 +1,165 @@ +getEditor(); + $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]); + + // Setup page state + $resp = $this->actingAs($editor)->get('/mfa/setup'); + $resp->assertElementContains('a[href$="/mfa/totp/generate"]', 'Setup'); + + // Generate page access + $resp = $this->get('/mfa/totp/generate'); + $resp->assertSee('Mobile App Setup'); + $resp->assertSee('Verify Setup'); + $resp->assertElementExists('form[action$="/mfa/totp/confirm"] button'); + $this->assertSessionHas('mfa-setup-totp-secret'); + $svg = $resp->getElementHtml('#main-content .card svg'); + + // Validation error, code should remain the same + $resp = $this->post('/mfa/totp/confirm', [ + 'code' => 'abc123', + ]); + $resp->assertRedirect('/mfa/totp/generate'); + $resp = $this->followRedirects($resp); + $resp->assertSee('The provided code is not valid or has expired.'); + $revisitSvg = $resp->getElementHtml('#main-content .card svg'); + $this->assertTrue($svg === $revisitSvg); + + // Successful confirmation + $google2fa = new Google2FA(); + $secret = decrypt(session()->get('mfa-setup-totp-secret')); + $otp = $google2fa->getCurrentOtp($secret); + $resp = $this->post('/mfa/totp/confirm', [ + 'code' => $otp, + ]); + $resp->assertRedirect('/mfa/setup'); + + // Confirmation of setup + $resp = $this->followRedirects($resp); + $resp->assertSee('Multi-factor method successfully configured'); + $resp->assertElementContains('a[href$="/mfa/totp/generate"]', 'Reconfigure'); + + $this->assertDatabaseHas('mfa_values', [ + 'user_id' => $editor->id, + 'method' => 'totp', + ]); + $this->assertFalse(session()->has('mfa-setup-totp-secret')); + $value = MfaValue::query()->where('user_id', '=', $editor->id) + ->where('method', '=', 'totp')->first(); + $this->assertEquals($secret, decrypt($value->value)); + } + + public function test_backup_codes_setup() + { + $editor = $this->getEditor(); + $this->assertDatabaseMissing('mfa_values', ['user_id' => $editor->id]); + + // Setup page state + $resp = $this->actingAs($editor)->get('/mfa/setup'); + $resp->assertElementContains('a[href$="/mfa/backup_codes/generate"]', 'Setup'); + + // Generate page access + $resp = $this->get('/mfa/backup_codes/generate'); + $resp->assertSee('Backup Codes'); + $resp->assertElementContains('form[action$="/mfa/backup_codes/confirm"]', 'Confirm and Enable'); + $this->assertSessionHas('mfa-setup-backup-codes'); + $codes = decrypt(session()->get('mfa-setup-backup-codes')); + // Check code format + $this->assertCount(16, $codes); + $this->assertEquals(16 * 11, strlen(implode('', $codes))); + // Check download link + $resp->assertSee(base64_encode(implode("\n\n", $codes))); + + // Confirm submit + $resp = $this->post('/mfa/backup_codes/confirm'); + $resp->assertRedirect('/mfa/setup'); + + // Confirmation of setup + $resp = $this->followRedirects($resp); + $resp->assertSee('Multi-factor method successfully configured'); + $resp->assertElementContains('a[href$="/mfa/backup_codes/generate"]', 'Reconfigure'); + + $this->assertDatabaseHas('mfa_values', [ + 'user_id' => $editor->id, + 'method' => 'backup_codes', + ]); + $this->assertFalse(session()->has('mfa-setup-backup-codes')); + $value = MfaValue::query()->where('user_id', '=', $editor->id) + ->where('method', '=', 'backup_codes')->first(); + $this->assertEquals($codes, json_decode(decrypt($value->value))); + } + + public function test_backup_codes_cannot_be_confirmed_if_not_previously_generated() + { + $resp = $this->asEditor()->post('/mfa/backup_codes/confirm'); + $resp->assertStatus(500); + } + + public function test_mfa_method_count_is_visible_on_user_edit_page() + { + $user = $this->getEditor(); + $resp = $this->actingAs($this->getAdmin())->get($user->getEditUrl()); + $resp->assertSee('0 methods configured'); + + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + $resp = $this->get($user->getEditUrl()); + $resp->assertSee('1 method configured'); + + MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, 'test'); + $resp = $this->get($user->getEditUrl()); + $resp->assertSee('2 methods configured'); + } + + public function test_mfa_setup_link_only_shown_when_viewing_own_user_edit_page() + { + $admin = $this->getAdmin(); + $resp = $this->actingAs($admin)->get($admin->getEditUrl()); + $resp->assertElementExists('a[href$="/mfa/setup"]'); + + $resp = $this->actingAs($admin)->get($this->getEditor()->getEditUrl()); + $resp->assertElementNotExists('a[href$="/mfa/setup"]'); + } + + public function test_mfa_indicator_shows_in_user_list() + { + $admin = $this->getAdmin(); + User::query()->where('id', '!=', $admin->id)->delete(); + + $resp = $this->actingAs($admin)->get('/settings/users'); + $resp->assertElementNotExists('[title="MFA Configured"] svg'); + + MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test'); + $resp = $this->actingAs($admin)->get('/settings/users'); + $resp->assertElementExists('[title="MFA Configured"] svg'); + } + + public function test_remove_mfa_method() + { + $admin = $this->getAdmin(); + + MfaValue::upsertWithValue($admin, MfaValue::METHOD_TOTP, 'test'); + $this->assertEquals(1, $admin->mfaValues()->count()); + $resp = $this->actingAs($admin)->get('/mfa/setup'); + $resp->assertElementExists('form[action$="/mfa/totp/remove"]'); + + $resp = $this->delete('/mfa/totp/remove'); + $resp->assertRedirect('/mfa/setup'); + $resp = $this->followRedirects($resp); + $resp->assertSee('Multi-factor method successfully removed'); + + $this->assertActivityExists(ActivityType::MFA_REMOVE_METHOD); + $this->assertEquals(0, $admin->mfaValues()->count()); + } +} diff --git a/tests/Auth/MfaVerificationTest.php b/tests/Auth/MfaVerificationTest.php new file mode 100644 index 000000000..ee6f3ecc8 --- /dev/null +++ b/tests/Auth/MfaVerificationTest.php @@ -0,0 +1,278 @@ +startTotpLogin(); + $loginResp->assertRedirect('/mfa/verify'); + + $resp = $this->get('/mfa/verify'); + $resp->assertSee('Verify Access'); + $resp->assertSee('Enter the code, generated using your mobile app, below:'); + $resp->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]'); + + $google2fa = new Google2FA(); + $resp = $this->post('/mfa/totp/verify', [ + 'code' => $google2fa->getCurrentOtp($secret), + ]); + $resp->assertRedirect('/'); + $this->assertEquals($user->id, auth()->user()->id); + } + + public function test_totp_verification_fails_on_missing_invalid_code() + { + [$user, $secret, $loginResp] = $this->startTotpLogin(); + + $resp = $this->get('/mfa/verify'); + $resp = $this->post('/mfa/totp/verify', [ + 'code' => '', + ]); + $resp->assertRedirect('/mfa/verify'); + + $resp = $this->get('/mfa/verify'); + $resp->assertSeeText('The code field is required.'); + $this->assertNull(auth()->user()); + + $resp = $this->post('/mfa/totp/verify', [ + 'code' => '123321', + ]); + $resp->assertRedirect('/mfa/verify'); + $resp = $this->get('/mfa/verify'); + + $resp->assertSeeText('The provided code is not valid or has expired.'); + $this->assertNull(auth()->user()); + } + + public function test_backup_code_verification() + { + [$user, $codes, $loginResp] = $this->startBackupCodeLogin(); + $loginResp->assertRedirect('/mfa/verify'); + + $resp = $this->get('/mfa/verify'); + $resp->assertSee('Verify Access'); + $resp->assertSee('Backup Code'); + $resp->assertSee('Enter one of your remaining backup codes below:'); + $resp->assertElementExists('form[action$="/mfa/backup_codes/verify"] input[name="code"]'); + + $resp = $this->post('/mfa/backup_codes/verify', [ + 'code' => $codes[1], + ]); + + $resp->assertRedirect('/'); + $this->assertEquals($user->id, auth()->user()->id); + // Ensure code no longer exists in available set + $userCodes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES); + $this->assertStringNotContainsString($codes[1], $userCodes); + $this->assertStringContainsString($codes[0], $userCodes); + } + + public function test_backup_code_verification_fails_on_missing_or_invalid_code() + { + [$user, $codes, $loginResp] = $this->startBackupCodeLogin(); + + $resp = $this->get('/mfa/verify'); + $resp = $this->post('/mfa/backup_codes/verify', [ + 'code' => '', + ]); + $resp->assertRedirect('/mfa/verify'); + + $resp = $this->get('/mfa/verify'); + $resp->assertSeeText('The code field is required.'); + $this->assertNull(auth()->user()); + + $resp = $this->post('/mfa/backup_codes/verify', [ + 'code' => 'ab123-ab456', + ]); + $resp->assertRedirect('/mfa/verify'); + + $resp = $this->get('/mfa/verify'); + $resp->assertSeeText('The provided code is not valid or has already been used.'); + $this->assertNull(auth()->user()); + } + + public function test_backup_code_verification_fails_on_attempted_code_reuse() + { + [$user, $codes, $loginResp] = $this->startBackupCodeLogin(); + + $this->post('/mfa/backup_codes/verify', [ + 'code' => $codes[0], + ]); + $this->assertNotNull(auth()->user()); + auth()->logout(); + session()->flush(); + + $this->post('/login', ['email' => $user->email, 'password' => 'password']); + $this->get('/mfa/verify'); + $resp = $this->post('/mfa/backup_codes/verify', [ + 'code' => $codes[0], + ]); + $resp->assertRedirect('/mfa/verify'); + $this->assertNull(auth()->user()); + + $resp = $this->get('/mfa/verify'); + $resp->assertSeeText('The provided code is not valid or has already been used.'); + } + + public function test_backup_code_verification_shows_warning_when_limited_codes_remain() + { + [$user, $codes, $loginResp] = $this->startBackupCodeLogin(['abc12-def45', 'abc12-def46']); + + $resp = $this->post('/mfa/backup_codes/verify', [ + 'code' => $codes[0], + ]); + $resp = $this->followRedirects($resp); + $resp->assertSeeText('You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.'); + } + + public function test_both_mfa_options_available_if_set_on_profile() + { + $user = $this->getEditor(); + $user->password = Hash::make('password'); + $user->save(); + + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'abc123'); + MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '["abc12-def456"]'); + + /** @var TestResponse $mfaView */ + $mfaView = $this->followingRedirects()->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + // Totp shown by default + $mfaView->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]'); + $mfaView->assertElementContains('a[href$="/mfa/verify?method=backup_codes"]', 'Verify using a backup code'); + + // Ensure can view backup_codes view + $resp = $this->get('/mfa/verify?method=backup_codes'); + $resp->assertElementExists('form[action$="/mfa/backup_codes/verify"] input[name="code"]'); + $resp->assertElementContains('a[href$="/mfa/verify?method=totp"]', 'Verify using a mobile app'); + } + + public function test_mfa_required_with_no_methods_leads_to_setup() + { + $user = $this->getEditor(); + $user->password = Hash::make('password'); + $user->save(); + /** @var Role $role */ + $role = $user->roles->first(); + $role->mfa_enforced = true; + $role->save(); + + $this->assertDatabaseMissing('mfa_values', [ + 'user_id' => $user->id, + ]); + + /** @var TestResponse $resp */ + $resp = $this->followingRedirects()->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $resp->assertSeeText('No Methods Configured'); + $resp->assertElementContains('a[href$="/mfa/setup"]', 'Configure'); + + $this->get('/mfa/backup_codes/generate'); + $resp = $this->post('/mfa/backup_codes/confirm'); + $resp->assertRedirect('/login'); + $this->assertDatabaseHas('mfa_values', [ + 'user_id' => $user->id, + ]); + + $resp = $this->get('/login'); + $resp->assertSeeText('Multi-factor method configured, Please now login again using the configured method.'); + + $resp = $this->followingRedirects()->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + $resp->assertSeeText('Enter one of your remaining backup codes below:'); + } + + public function test_mfa_setup_route_access() + { + $routes = [ + ['get', '/mfa/setup'], + ['get', '/mfa/totp/generate'], + ['post', '/mfa/totp/confirm'], + ['get', '/mfa/backup_codes/generate'], + ['post', '/mfa/backup_codes/confirm'], + ]; + + // Non-auth access + foreach ($routes as [$method, $path]) { + $resp = $this->call($method, $path); + $resp->assertRedirect('/login'); + } + + // Attempted login user, who has configured mfa, access + // Sets up user that has MFA required after attempted login. + $loginService = $this->app->make(LoginService::class); + $user = $this->getEditor(); + /** @var Role $role */ + $role = $user->roles->first(); + $role->mfa_enforced = true; + $role->save(); + + try { + $loginService->login($user, 'testing'); + } catch (StoppedAuthenticationException $e) { + } + $this->assertNotNull($loginService->getLastLoginAttemptUser()); + + MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, '[]'); + foreach ($routes as [$method, $path]) { + $resp = $this->call($method, $path); + $resp->assertRedirect('/login'); + } + } + + /** + * @return Array + */ + protected function startTotpLogin(): array + { + $secret = $this->app->make(TotpService::class)->generateSecret(); + $user = $this->getEditor(); + $user->password = Hash::make('password'); + $user->save(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, $secret); + $loginResp = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + return [$user, $secret, $loginResp]; + } + + /** + * @return Array + */ + protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array + { + $user = $this->getEditor(); + $user->password = Hash::make('password'); + $user->save(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, json_encode($codes)); + $loginResp = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + return [$user, $codes, $loginResp]; + } +} diff --git a/tests/Auth/Saml2Test.php b/tests/Auth/Saml2Test.php index b6b02e2f7..8ace3e2ee 100644 --- a/tests/Auth/Saml2Test.php +++ b/tests/Auth/Saml2Test.php @@ -1,4 +1,6 @@ -set([ - 'auth.method' => 'saml2', - 'auth.defaults.guard' => 'saml2', - 'saml2.name' => 'SingleSignOn-Testing', - 'saml2.email_attribute' => 'email', - 'saml2.display_name_attributes' => ['first_name', 'last_name'], - 'saml2.external_id_attribute' => 'uid', - 'saml2.user_to_groups' => false, - 'saml2.group_attribute' => 'user_groups', - 'saml2.remove_from_groups' => false, - 'saml2.onelogin_overrides' => null, - 'saml2.onelogin.idp.entityId' => 'http://saml.local/saml2/idp/metadata.php', - 'saml2.onelogin.idp.singleSignOnService.url' => 'http://saml.local/saml2/idp/SSOService.php', - 'saml2.onelogin.idp.singleLogoutService.url' => 'http://saml.local/saml2/idp/SingleLogoutService.php', - 'saml2.autoload_from_metadata' => false, - 'saml2.onelogin.idp.x509cert' => $this->testCert, - 'saml2.onelogin.debug' => false, + 'auth.method' => 'saml2', + 'auth.defaults.guard' => 'saml2', + 'saml2.name' => 'SingleSignOn-Testing', + 'saml2.email_attribute' => 'email', + 'saml2.display_name_attributes' => ['first_name', 'last_name'], + 'saml2.external_id_attribute' => 'uid', + 'saml2.user_to_groups' => false, + 'saml2.group_attribute' => 'user_groups', + 'saml2.remove_from_groups' => false, + 'saml2.onelogin_overrides' => null, + 'saml2.onelogin.idp.entityId' => 'http://saml.local/saml2/idp/metadata.php', + 'saml2.onelogin.idp.singleSignOnService.url' => 'http://saml.local/saml2/idp/SSOService.php', + 'saml2.onelogin.idp.singleLogoutService.url' => 'http://saml.local/saml2/idp/SingleLogoutService.php', + 'saml2.autoload_from_metadata' => false, + 'saml2.onelogin.idp.x509cert' => $this->testCert, + 'saml2.onelogin.debug' => false, 'saml2.onelogin.security.requestedAuthnContext' => true, ]); } @@ -68,25 +69,23 @@ class Saml2Test extends TestCase $this->assertFalse($this->isAuthenticated()); $this->withPost(['SAMLResponse' => $this->acsPostData], function () { - $acsPost = $this->post('/saml2/acs'); $acsPost->assertRedirect('/'); $this->assertTrue($this->isAuthenticated()); $this->assertDatabaseHas('users', [ - 'email' => 'user@example.com', + 'email' => 'user@example.com', 'external_auth_id' => 'user', - 'email_confirmed' => false, - 'name' => 'Barry Scott' + 'email_confirmed' => false, + 'name' => 'Barry Scott', ]); - }); } public function test_group_role_sync_on_login() { config()->set([ - 'saml2.onelogin.strict' => false, - 'saml2.user_to_groups' => true, + 'saml2.onelogin.strict' => false, + 'saml2.user_to_groups' => true, 'saml2.remove_from_groups' => false, ]); @@ -106,8 +105,8 @@ class Saml2Test extends TestCase public function test_group_role_sync_removal_option_works_as_expected() { config()->set([ - 'saml2.onelogin.strict' => false, - 'saml2.user_to_groups' => true, + 'saml2.onelogin.strict' => false, + 'saml2.user_to_groups' => true, 'saml2.remove_from_groups' => true, ]); @@ -165,7 +164,7 @@ class Saml2Test extends TestCase public function test_logout_sls_flow_when_sls_not_configured() { config()->set([ - 'saml2.onelogin.strict' => false, + 'saml2.onelogin.strict' => false, 'saml2.onelogin.idp.singleLogoutService.url' => null, ]); @@ -183,7 +182,7 @@ class Saml2Test extends TestCase public function test_dump_user_details_option_works() { config()->set([ - 'saml2.onelogin.strict' => false, + 'saml2.onelogin.strict' => false, 'saml2.dump_user_details' => true, ]); @@ -191,7 +190,7 @@ class Saml2Test extends TestCase $acsPost = $this->post('/saml2/acs'); $acsPost->assertJsonStructure([ 'id_from_idp', - 'attrs_from_idp' => [], + 'attrs_from_idp' => [], 'attrs_after_parsing' => [], ]); }); @@ -258,7 +257,7 @@ class Saml2Test extends TestCase public function test_email_domain_restriction_active_on_new_saml_login() { $this->setSettings([ - 'registration-restrict' => 'testing.com' + 'registration-restrict' => 'testing.com', ]); config()->set([ 'saml2.onelogin.strict' => false, @@ -277,8 +276,8 @@ class Saml2Test extends TestCase { setting()->put('registration-confirmation', 'true'); config()->set([ - 'saml2.onelogin.strict' => false, - 'saml2.user_to_groups' => true, + 'saml2.onelogin.strict' => false, + 'saml2.user_to_groups' => true, 'saml2.remove_from_groups' => false, ]); @@ -290,16 +289,18 @@ class Saml2Test extends TestCase $this->assertEquals('http://localhost/register/confirm', url()->current()); $acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.'); + /** @var User $user */ $user = User::query()->where('external_auth_id', '=', 'user')->first(); $userRoleIds = $user->roles()->pluck('id'); $this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role'); $this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role'); - $this->assertTrue($user->email_confirmed == false, 'User email remains unconfirmed'); + $this->assertFalse(boolval($user->email_confirmed), 'User email remains unconfirmed'); }); + $this->assertNull(auth()->user()); $homeGet = $this->get('/'); - $homeGet->assertRedirect('/register/confirm/awaiting'); + $homeGet->assertRedirect('/login'); } public function test_login_where_existing_non_saml_user_shows_warning() @@ -309,10 +310,10 @@ class Saml2Test extends TestCase // Make the user pre-existing in DB with different auth_id User::query()->forceCreate([ - 'email' => 'user@example.com', + 'email' => 'user@example.com', 'external_auth_id' => 'old_system_user_id', - 'email_confirmed' => false, - 'name' => 'Barry Scott' + 'email_confirmed' => false, + 'name' => 'Barry Scott', ]); $this->withPost(['SAMLResponse' => $this->acsPostData], function () { @@ -320,12 +321,12 @@ class Saml2Test extends TestCase $acsPost->assertRedirect('/login'); $this->assertFalse($this->isAuthenticated()); $this->assertDatabaseHas('users', [ - 'email' => 'user@example.com', + 'email' => 'user@example.com', 'external_auth_id' => 'old_system_user_id', ]); $loginGet = $this->get('/login'); - $loginGet->assertSee("A user with the email user@example.com already exists but with different credentials"); + $loginGet->assertSee('A user with the email user@example.com already exists but with different credentials'); }); } @@ -360,6 +361,7 @@ class Saml2Test extends TestCase $query = explode('?', $location)[1]; $params = []; parse_str($query, $params); + return gzinflate(base64_decode($params['SAMLRequest'])); } @@ -397,23 +399,23 @@ class Saml2Test extends TestCase * The post data for a callback for single-sign-in. * Provides the following attributes: * array:5 [ - "uid" => array:1 [ - 0 => "user" - ] - "first_name" => array:1 [ - 0 => "Barry" - ] - "last_name" => array:1 [ - 0 => "Scott" - ] - "email" => array:1 [ - 0 => "user@example.com" - ] - "user_groups" => array:2 [ - 0 => "member" - 1 => "admin" - ] - ] + * "uid" => array:1 [ + * 0 => "user" + * ] + * "first_name" => array:1 [ + * 0 => "Barry" + * ] + * "last_name" => array:1 [ + * 0 => "Scott" + * ] + * "email" => array:1 [ + * 0 => "user@example.com" + * ] + * "user_groups" => array:2 [ + * 0 => "member" + * 1 => "admin" + * ] + * ]. */ protected $acsPostData = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxOS0xMS0xN1QxNzo1MzozOVoiIERlc3RpbmF0aW9uPSJodHRwOi8vYm9va3N0YWNrLmxvY2FsL3NhbWwyL2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YTBmNGYzOTkzMDQwZjE5ODdmZDM3MDY4YjUyOTYyMjlhZDUzNjFjIj48c2FtbDpJc3N1ZXI+aHR0cDovL3NhbWwubG9jYWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+dm1oL1M3NU5mK2crZWNESkN6QWJaV0tKVmx1ZzdCZnNDKzlhV05lSXJlUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dnJhZ0tKWHNjVm5UNjJFaEk3bGk4MERUWHNOTGJOc3lwNWZ2QnU4WjFYSEtFUVA3QWpPNkcxcVBwaGpWQ2dRMzd6TldVVTZvUytQeFA3UDlHeG5xL3hKejRUT3lHcHJ5N1RoK2pIcHc0YWVzQTdrTmp6VU51UmU2c1ltWTlrRXh2VjMvTmJRZjROMlM2Y2RhRHIzWFRodllVVDcxYzQwNVVHOFJpQjJaY3liWHIxZU1yWCtXUDBnU2Qrc0F2RExqTjBJc3pVWlVUNThadFpEVE1ya1ZGL0pIbFBFQ04vVW1sYVBBeitTcUJ4c25xTndZK1oxYUt3MnlqeFRlNnUxM09Kb29OOVN1REowNE0rK2F3RlY3NkI4cXEyTzMxa3FBbDJibm1wTGxtTWdRNFEraUlnL3dCc09abTV1clphOWJObDNLVEhtTVBXbFpkbWhsLzgvMy9IT1RxN2thWGs3cnlWRHRLcFlsZ3FUajNhRUpuL0dwM2o4SFp5MUVialRiOTRRT1ZQMG5IQzB1V2hCaE13TjdzVjFrUSsxU2NjUlpUZXJKSGlSVUQvR0srTVg3M0YrbzJVTFRIL1Z6Tm9SM2o4N2hOLzZ1UC9JeG5aM1RudGR1MFZPZS9ucEdVWjBSMG9SWFhwa2JTL2poNWk1ZjU0RXN4eXZ1VEM5NHdKaEM8L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFYXpDQ0F0T2dBd0lCQWdJVWU3YTA4OENucjRpem1ybkJFbng1cTNIVE12WXdEUVlKS29aSWh2Y05BUUVMQlFBd1JURUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTUdFbHVkR1Z5Ym1WMElGZHBaR2RwZEhNZ1VIUjVJRXgwWkRBZUZ3MHhPVEV4TVRZeE1qRTNNVFZhRncweU9URXhNVFV4TWpFM01UVmFNRVV4Q3pBSkJnTlZCQVlUQWtkQ01STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FEekxlOUZmZHlwbFR4SHA0U3VROWdRdFpUM3QrU0RmdkVMNzJwcENmRlp3NytCNXM1Qi9UNzNhWHBvUTNTNTNwR0kxUklXQ2dlMmlDVVEydHptMjdhU05IMGl1OWFKWWNVUVovUklUcWQwYXl5RGtzMU5BMlBUM1RXNnQzbTdLVjVyZTRQME5iK1lEZXV5SGRreitqY010cG44Q21Cb1QwSCtza2hhMGhpcUlOa2prUlBpSHZMSFZHcCt0SFVFQS9JNm1ONGFCL1VFeFNUTHM3OU5zTFVmdGVxcXhlOSt0dmRVYVRveURQcmhQRmpPTnMrOU5LQ2t6SUM2dmN2N0o2QXR1S0c2bkVUK3pCOXlPV2d0R1lRaWZYcVFBMnk1ZEw4MUJCMHE1dU1hQkxTMnBxM2FQUGp6VTJGMytFeXNqeVNXVG5Da2ZrN0M1U3NDWFJ1OFErVTk1dHVucE5md2Y1b2xFNldhczQ4Tk1NK1B3VjdpQ05NUGtOemxscTZQQ2lNK1A4RHJNU2N6elVaWlFVU3Y2ZFN3UENvK1lTVmltRU0wT2czWEpUaU5oUTVBTmxhSW42Nkt3NWdmb0JmdWlYbXlJS2lTRHlBaURZbUZhZjQzOTV3V3dMa1RSK2N3OFdmamFIc3dLWlRvbW4xTVIzT0pzWTJVSjBlUkJZTStZU3NDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01COEdBMVVkSXdRWU1CYUFGSW1wMkNZQ0dmY2I3dzkxSC9jU2hUQ2tYd1IvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUErZy9DN3VMOWxuK1crcUJrbkxXODFrb2pZZmxnUEsxSTFNSEl3bk12bC9aVEhYNGRSWEtEcms3S2NVcTFLanFhak5WNjZmMWNha3AwM0lpakJpTzBYaTFnWFVaWUxvQ2lOR1V5eXA5WGxvaUl5OVh3MlBpV25ydzAreVp5dlZzc2JlaFhYWUpsNFJpaEJqQld1bDlSNHdNWUxPVVNKRGUyV3hjVUJoSm54eU5ScytQMHhMU1FYNkIybjZueG9Ea280cDA3czhaS1hRa2VpWjJpd0ZkVHh6UmtHanRoTVV2NzA0bnpzVkdCVDBEQ1B0ZlNhTzVLSlpXMXJDczN5aU10aG5CeHE0cUVET1FKRklsKy9MRDcxS2JCOXZaY1c1SnVhdnpCRm1rS0dOcm8vNkcxSTdlbDQ2SVI0d2lqVHlORkNZVXVEOWR0aWduTm1wV3ROOE9XK3B0aUwvanRUeVNXdWtqeXMwcyt2TG44M0NWdmpCMGRKdFZBSVlPZ1hGZEl1aWk2Nmdjend3TS9MR2lPRXhKbjBkVE56c0ovSVlocHhMNEZCRXVQMHBza1kwbzBhVWxKMkxTMmord1NRVFJLc0JnTWp5clVyZWtsZTJPRFN0U3RuM2VhYmpJeDAvRkhscEZyMGpOSW0vb01QN2t3anRVWDR6YU5lNDdRSTRHZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il82ODQyZGY5YzY1OWYxM2ZlNTE5NmNkOWVmNmMyZjAyODM2NGFlOTQzYjEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE5LTExLTE3VDE3OjUzOjM5WiI+PHNhbWw6SXNzdWVyPmh0dHA6Ly9zYW1sLmxvY2FsL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjXzY4NDJkZjljNjU5ZjEzZmU1MTk2Y2Q5ZWY2YzJmMDI4MzY0YWU5NDNiMSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPmtyYjV3NlM4dG9YYy9lU3daUFVPQnZRem4zb3M0SkFDdXh4ckpreHBnRnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPjJxcW1Ba3hucXhOa3N5eXh5dnFTVDUxTDg5VS9ZdHpja2t1ekF4ci9hQ1JTK1NPRzg1YkFNWm8vU3puc3d0TVlBYlFRQ0VGb0R1amdNdlpzSFl3NlR2dmFHanlXWUpRNVZyYWhlemZaSWlCVUU0NHBtWGFrOCswV0l0WTVndnBGSXhxWFZaRmdFUkt2VExmZVFCMzhkMVZQc0ZVZ0RYdXQ4VS9Qdm43dXZwdXZjVXorMUUyOUVKR2FZL0dndnhUN0tyWU9SQTh3SitNdVRzUVZtanNlUnhveVJTejA4TmJ3ZTJIOGpXQnpFWWNxWWwyK0ZnK2hwNWd0S216VmhLRnBkNXZBNjdBSXo1NXN0QmNHNSswNHJVaWpFSzRzci9xa0x5QmtKQjdLdkwzanZKcG8zQjhxYkxYeXhLb1dSSmRnazhKNHMvTVp1QWk3QWUxUXNTTjl2Z3ZTdVRlc0VCUjVpSHJuS1lrbEpRWXNrbUQzbSsremE4U1NRbnBlM0UzYUZBY3p6cElUdUQ4YkFCWmRqcUk2TkhrSmFRQXBmb0hWNVQrZ244ejdUTWsrSStUU2JlQURubUxCS3lnMHRabW10L0ZKbDV6eWowVmxwc1dzTVM2OVE2bUZJVStqcEhSanpOb2FLMVM1dlQ3ZW1HbUhKSUp0cWlOdXJRN0tkQlBJPC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRWF6Q0NBdE9nQXdJQkFnSVVlN2EwODhDbnI0aXptcm5CRW54NXEzSFRNdll3RFFZSktvWklodmNOQVFFTEJRQXdSVEVMTUFrR0ExVUVCaE1DUjBJeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWVGdzB4T1RFeE1UWXhNakUzTVRWYUZ3MHlPVEV4TVRVeE1qRTNNVFZhTUVVeEN6QUpCZ05WQkFZVEFrZENNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3Z2dHaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQmp3QXdnZ0dLQW9JQmdRRHpMZTlGZmR5cGxUeEhwNFN1UTlnUXRaVDN0K1NEZnZFTDcycHBDZkZadzcrQjVzNUIvVDczYVhwb1EzUzUzcEdJMVJJV0NnZTJpQ1VRMnR6bTI3YVNOSDBpdTlhSlljVVFaL1JJVHFkMGF5eURrczFOQTJQVDNUVzZ0M203S1Y1cmU0UDBOYitZRGV1eUhka3oramNNdHBuOENtQm9UMEgrc2toYTBoaXFJTmtqa1JQaUh2TEhWR3ArdEhVRUEvSTZtTjRhQi9VRXhTVExzNzlOc0xVZnRlcXF4ZTkrdHZkVWFUb3lEUHJoUEZqT05zKzlOS0NreklDNnZjdjdKNkF0dUtHNm5FVCt6Qjl5T1dndEdZUWlmWHFRQTJ5NWRMODFCQjBxNXVNYUJMUzJwcTNhUFBqelUyRjMrRXlzanlTV1RuQ2tmazdDNVNzQ1hSdThRK1U5NXR1bnBOZndmNW9sRTZXYXM0OE5NTStQd1Y3aUNOTVBrTnpsbHE2UENpTStQOERyTVNjenpVWlpRVVN2NmRTd1BDbytZU1ZpbUVNME9nM1hKVGlOaFE1QU5sYUluNjZLdzVnZm9CZnVpWG15SUtpU0R5QWlEWW1GYWY0Mzk1d1d3TGtUUitjdzhXZmphSHN3S1pUb21uMU1SM09Kc1kyVUowZVJCWU0rWVNzQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZJbXAyQ1lDR2ZjYjd3OTFIL2NTaFRDa1h3Ui9NQjhHQTFVZEl3UVlNQmFBRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dHQkFBK2cvQzd1TDlsbitXK3FCa25MVzgxa29qWWZsZ1BLMUkxTUhJd25NdmwvWlRIWDRkUlhLRHJrN0tjVXExS2pxYWpOVjY2ZjFjYWtwMDNJaWpCaU8wWGkxZ1hVWllMb0NpTkdVeXlwOVhsb2lJeTlYdzJQaVducncwK3laeXZWc3NiZWhYWFlKbDRSaWhCakJXdWw5UjR3TVlMT1VTSkRlMld4Y1VCaEpueHlOUnMrUDB4TFNRWDZCMm42bnhvRGtvNHAwN3M4WktYUWtlaVoyaXdGZFR4elJrR2p0aE1VdjcwNG56c1ZHQlQwRENQdGZTYU81S0paVzFyQ3MzeWlNdGhuQnhxNHFFRE9RSkZJbCsvTEQ3MUtiQjl2WmNXNUp1YXZ6QkZta0tHTnJvLzZHMUk3ZWw0NklSNHdpalR5TkZDWVV1RDlkdGlnbk5tcFd0TjhPVytwdGlML2p0VHlTV3VranlzMHMrdkxuODNDVnZqQjBkSnRWQUlZT2dYRmRJdWlpNjZnY3p3d00vTEdpT0V4Sm4wZFROenNKL0lZaHB4TDRGQkV1UDBwc2tZMG8wYVVsSjJMUzJqK3dTUVRSS3NCZ01qeXJVcmVrbGUyT0RTdFN0bjNlYWJqSXgwL0ZIbHBGcjBqTkltL29NUDdrd2p0VVg0emFOZTQ3UUk0R2c9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiPl8yYzdhYjg2ZWI4ZjFkMTA2MzQ0M2YyMTljYzU4NjhmZjY2NzA4OTEyZTM8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTktMTEtMTdUMTc6NTg6MzlaIiBSZWNpcGllbnQ9Imh0dHA6Ly9ib29rc3RhY2subG9jYWwvc2FtbDIvYWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzZhMGY0ZjM5OTMwNDBmMTk4N2ZkMzcwNjhiNTI5NjIyOWFkNTM2MWMiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOS0xMS0xN1QxNzo1MzowOVoiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMS0xN1QxNzo1ODozOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTktMTEtMTdUMTc6NTM6MzlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE5LTExLTE4VDAxOjUzOjM5WiIgU2Vzc2lvbkluZGV4PSJfNGZlN2MwZDE1NzJkNjRiMjdmOTMwYWE2ZjIzNmE2ZjQyZTkzMDkwMWNjIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZmlyc3RfbmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+QmFycnk8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibGFzdF9uYW1lIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5TY290dDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlbWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1c2VyX2dyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+bWVtYmVyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+'; diff --git a/tests/Auth/SocialAuthTest.php b/tests/Auth/SocialAuthTest.php index 60de8fbcb..5818cbb74 100644 --- a/tests/Auth/SocialAuthTest.php +++ b/tests/Auth/SocialAuthTest.php @@ -1,4 +1,6 @@ -make(); @@ -43,7 +44,7 @@ class SocialAuthTest extends TestCase config([ 'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', - 'APP_URL' => 'http://localhost' + 'APP_URL' => 'http://localhost', ]); $mockSocialite = $this->mock(Factory::class); @@ -60,7 +61,7 @@ class SocialAuthTest extends TestCase // Test login routes $resp = $this->get('/login'); $resp->assertElementExists('a#social-login-google[href$="/login/service/google"]'); - $resp = $this->followingRedirects()->get("/login/service/google"); + $resp = $this->followingRedirects()->get('/login/service/google'); $resp->assertSee('login-form'); // Test social callback @@ -70,18 +71,17 @@ class SocialAuthTest extends TestCase $resp = $this->get('/login'); $resp->assertElementExists('a#social-login-github[href$="/login/service/github"]'); - $resp = $this->followingRedirects()->get("/login/service/github"); + $resp = $this->followingRedirects()->get('/login/service/github'); $resp->assertSee('login-form'); - // Test social callback with matching social account DB::table('social_accounts')->insert([ - 'user_id' => $this->getAdmin()->id, - 'driver' => 'github', - 'driver_id' => 'logintest123' + 'user_id' => $this->getAdmin()->id, + 'driver' => 'github', + 'driver_id' => 'logintest123', ]); $resp = $this->followingRedirects()->get('/login/service/github/callback'); - $resp->assertDontSee("login-form"); + $resp->assertDontSee('login-form'); } public function test_social_account_detach() @@ -89,12 +89,12 @@ class SocialAuthTest extends TestCase $editor = $this->getEditor(); config([ 'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', - 'APP_URL' => 'http://localhost' + 'APP_URL' => 'http://localhost', ]); $socialAccount = SocialAccount::query()->forceCreate([ - 'user_id' => $editor->id, - 'driver' => 'github', + 'user_id' => $editor->id, + 'driver' => 'github', 'driver_id' => 'logintest123', ]); @@ -113,7 +113,7 @@ class SocialAuthTest extends TestCase { config([ 'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc', - 'APP_URL' => 'http://localhost' + 'APP_URL' => 'http://localhost', ]); $user = factory(User::class)->make(); @@ -151,7 +151,7 @@ class SocialAuthTest extends TestCase { config([ 'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc', - 'APP_URL' => 'http://localhost', 'services.google.auto_register' => true, 'services.google.auto_confirm' => true + 'APP_URL' => 'http://localhost', 'services.google.auto_register' => true, 'services.google.auto_confirm' => true, ]); $user = factory(User::class)->make(); @@ -210,5 +210,4 @@ class SocialAuthTest extends TestCase $user = $user->whereEmail($user->email)->first(); $this->assertDatabaseHas('social_accounts', ['user_id' => $user->id]); } - } diff --git a/tests/Auth/UserInviteTest.php b/tests/Auth/UserInviteTest.php index b6f521eaa..c5c4b01af 100644 --- a/tests/Auth/UserInviteTest.php +++ b/tests/Auth/UserInviteTest.php @@ -1,5 +1,6 @@ -actingAs($admin)->post('/settings/users/create', [ - 'name' => 'Barry', - 'email' => $email, + 'name' => 'Barry', + 'email' => $email, 'send_invite' => 'true', ]); $resp->assertRedirect('/settings/users'); @@ -30,7 +30,7 @@ class UserInviteTest extends TestCase Notification::assertSentTo($newUser, UserInvite::class); $this->assertDatabaseHas('user_invites', [ - 'user_id' => $newUser->id + 'user_id' => $newUser->id, ]); } @@ -54,12 +54,12 @@ class UserInviteTest extends TestCase ]); $setPasswordResp->assertSee('Password set, you now have access to BookStack!'); $newPasswordValid = auth()->validate([ - 'email' => $user->email, - 'password' => 'my test password' + 'email' => $user->email, + 'password' => 'my test password', ]); $this->assertTrue($newPasswordValid); $this->assertDatabaseMissing('user_invites', [ - 'user_id' => $user->id + 'user_id' => $user->id, ]); } @@ -85,7 +85,7 @@ class UserInviteTest extends TestCase $noPassword->assertSee('The password field is required.'); $this->assertDatabaseHas('user_invites', [ - 'user_id' => $user->id + 'user_id' => $user->id, ]); } @@ -112,6 +112,4 @@ class UserInviteTest extends TestCase $setPasswordPageResp->assertRedirect('/password/email'); $setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.'); } - - -} \ No newline at end of file +} diff --git a/tests/BrowserKitTest.php b/tests/BrowserKitTest.php index 7917a0c40..23eb10887 100644 --- a/tests/BrowserKitTest.php +++ b/tests/BrowserKitTest.php @@ -1,10 +1,12 @@ -make(Kernel::class)->bootstrap(); return $app; } - /** * Quickly sets an array of settings. + * * @param $settingsArray */ protected function setSettings($settingsArray) @@ -80,6 +82,7 @@ abstract class BrowserKitTest extends TestCase /** * Helper for updating entity permissions. + * * @param Entity $entity */ protected function updateEntityPermissions(Entity $entity) @@ -88,25 +91,28 @@ abstract class BrowserKitTest extends TestCase $restrictionService->buildJointPermissionsForEntity($entity); } - /** - * Quick way to create a new user without any permissions + * Quick way to create a new user without any permissions. + * * @param array $attributes + * * @return mixed */ protected function getNewBlankUser($attributes = []) { $user = factory(User::class)->create($attributes); + return $user; } /** * Assert that a given string is seen inside an element. * - * @param bool|string|null $element - * @param integer $position - * @param string $text - * @param bool $negate + * @param bool|string|null $element + * @param int $position + * @param string $text + * @param bool $negate + * * @return $this */ protected function seeInNthElement($element, $position, $text, $negate = false) @@ -130,13 +136,16 @@ abstract class BrowserKitTest extends TestCase /** * Assert that the current page matches a given URI. * - * @param string $uri + * @param string $uri + * * @return $this */ protected function seePageUrlIs($uri) { $this->assertEquals( - $uri, $this->currentUri, "Did not land on expected page [{$uri}].\n" + $uri, + $this->currentUri, + "Did not land on expected page [{$uri}].\n" ); return $this; @@ -144,10 +153,12 @@ abstract class BrowserKitTest extends TestCase /** * Do a forced visit that does not error out on exception. + * * @param string $uri - * @param array $parameters - * @param array $cookies - * @param array $files + * @param array $parameters + * @param array $cookies + * @param array $files + * * @return $this */ protected function forceVisit($uri, $parameters = [], $cookies = [], $files = []) @@ -158,13 +169,16 @@ abstract class BrowserKitTest extends TestCase $this->clearInputs()->followRedirects(); $this->currentUri = $this->app->make('request')->fullUrl(); $this->crawler = new Crawler($this->response->getContent(), $uri); + return $this; } /** * Click the text within the selected element. + * * @param $parentElement * @param $linkText + * * @return $this */ protected function clickInElement($parentElement, $linkText) @@ -172,28 +186,33 @@ abstract class BrowserKitTest extends TestCase $elem = $this->crawler->filter($parentElement); $link = $elem->selectLink($linkText); $this->visit($link->link()->getUri()); + return $this; } /** * Check if the page contains the given element. - * @param string $selector + * + * @param string $selector */ protected function pageHasElement($selector) { $elements = $this->crawler->filter($selector); - $this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector); + $this->assertTrue(count($elements) > 0, 'The page does not contain an element matching ' . $selector); + return $this; } /** * Check if the page contains the given element. - * @param string $selector + * + * @param string $selector */ protected function pageNotHasElement($selector) { $elements = $this->crawler->filter($selector); - $this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector); + $this->assertFalse(count($elements) > 0, 'The page contains ' . count($elements) . ' elements matching ' . $selector); + return $this; } } diff --git a/tests/Commands/AddAdminCommandTest.php b/tests/Commands/AddAdminCommandTest.php index 6b03c86f9..0f144246c 100644 --- a/tests/Commands/AddAdminCommandTest.php +++ b/tests/Commands/AddAdminCommandTest.php @@ -1,4 +1,6 @@ - 'admintest@example.com', - '--name' => 'Admin Test', + '--email' => 'admintest@example.com', + '--name' => 'Admin Test', '--password' => 'testing-4', ]); $this->assertTrue($exitCode === 0, 'Command executed successfully'); $this->assertDatabaseHas('users', [ 'email' => 'admintest@example.com', - 'name' => 'Admin Test' + 'name' => 'Admin Test', ]); $this->assertTrue(User::query()->where('email', '=', 'admintest@example.com')->first()->hasSystemRole('admin'), 'User has admin role as expected'); $this->assertTrue(\Auth::attempt(['email' => 'admintest@example.com', 'password' => 'testing-4']), 'Password stored as expected'); } -} \ No newline at end of file +} diff --git a/tests/Commands/ClearActivityCommandTest.php b/tests/Commands/ClearActivityCommandTest.php index 751a165c6..172e6c6ae 100644 --- a/tests/Commands/ClearActivityCommandTest.php +++ b/tests/Commands/ClearActivityCommandTest.php @@ -1,4 +1,6 @@ -assertDatabaseHas('activities', [ - 'type' => 'page_update', + 'type' => 'page_update', 'entity_id' => $page->id, - 'user_id' => $this->getEditor()->id + 'user_id' => $this->getEditor()->id, ]); - DB::rollBack(); $exitCode = \Artisan::call('bookstack:clear-activity'); DB::beginTransaction(); $this->assertTrue($exitCode === 0, 'Command executed successfully'); - $this->assertDatabaseMissing('activities', [ - 'type' => 'page_update' + 'type' => 'page_update', ]); } -} \ No newline at end of file +} diff --git a/tests/Commands/ClearRevisionsCommandTest.php b/tests/Commands/ClearRevisionsCommandTest.php index e0293faf1..a7aef958f 100644 --- a/tests/Commands/ClearRevisionsCommandTest.php +++ b/tests/Commands/ClearRevisionsCommandTest.php @@ -1,4 +1,6 @@ -assertDatabaseHas('page_revisions', [ 'page_id' => $page->id, - 'type' => 'version' + 'type' => 'version', ]); $this->assertDatabaseHas('page_revisions', [ 'page_id' => $page->id, - 'type' => 'update_draft' + 'type' => 'update_draft', ]); $exitCode = Artisan::call('bookstack:clear-revisions'); @@ -29,11 +31,11 @@ class ClearRevisionsCommandTest extends TestCase $this->assertDatabaseMissing('page_revisions', [ 'page_id' => $page->id, - 'type' => 'version' + 'type' => 'version', ]); $this->assertDatabaseHas('page_revisions', [ 'page_id' => $page->id, - 'type' => 'update_draft' + 'type' => 'update_draft', ]); $exitCode = Artisan::call('bookstack:clear-revisions', ['--all' => true]); @@ -41,7 +43,7 @@ class ClearRevisionsCommandTest extends TestCase $this->assertDatabaseMissing('page_revisions', [ 'page_id' => $page->id, - 'type' => 'update_draft' + 'type' => 'update_draft', ]); } -} \ No newline at end of file +} diff --git a/tests/Commands/ClearViewsCommandTest.php b/tests/Commands/ClearViewsCommandTest.php index 04665adcf..bbd06fa01 100644 --- a/tests/Commands/ClearViewsCommandTest.php +++ b/tests/Commands/ClearViewsCommandTest.php @@ -1,4 +1,6 @@ -asEditor(); @@ -15,9 +16,9 @@ class ClearViewsCommandTest extends TestCase $this->get($page->getUrl()); $this->assertDatabaseHas('views', [ - 'user_id' => $this->getEditor()->id, + 'user_id' => $this->getEditor()->id, 'viewable_id' => $page->id, - 'views' => 1 + 'views' => 1, ]); DB::rollBack(); @@ -26,7 +27,7 @@ class ClearViewsCommandTest extends TestCase $this->assertTrue($exitCode === 0, 'Command executed successfully'); $this->assertDatabaseMissing('views', [ - 'user_id' => $this->getEditor()->id + 'user_id' => $this->getEditor()->id, ]); } -} \ No newline at end of file +} diff --git a/tests/Commands/CopyShelfPermissionsCommandTest.php b/tests/Commands/CopyShelfPermissionsCommandTest.php index 87199bdc3..5a60b8d55 100644 --- a/tests/Commands/CopyShelfPermissionsCommandTest.php +++ b/tests/Commands/CopyShelfPermissionsCommandTest.php @@ -1,4 +1,6 @@ -books()->first(); $editorRole = $this->getEditor()->roles()->first(); - $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default"); - $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default"); + $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); + $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); $this->artisan('bookstack:copy-shelf-permissions', [ @@ -26,8 +28,8 @@ class CopyShelfPermissionsCommandTest extends TestCase ]); $child = $shelf->books()->first(); - $this->assertTrue(boolval($child->restricted), "Child book should now be restricted"); - $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions"); + $this->assertTrue(boolval($child->restricted), 'Child book should now be restricted'); + $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions'); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]); } @@ -38,17 +40,17 @@ class CopyShelfPermissionsCommandTest extends TestCase Bookshelf::query()->where('id', '!=', $shelf->id)->delete(); $child = $shelf->books()->first(); $editorRole = $this->getEditor()->roles()->first(); - $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default"); - $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default"); + $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); + $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); $this->artisan('bookstack:copy-shelf-permissions --all') ->expectsQuestion('Permission settings for all shelves will be cascaded. Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. Are you sure you want to proceed?', 'y'); $child = $shelf->books()->first(); - $this->assertTrue(boolval($child->restricted), "Child book should now be restricted"); - $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions"); + $this->assertTrue(boolval($child->restricted), 'Child book should now be restricted'); + $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions'); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]); } -} \ No newline at end of file +} diff --git a/tests/Commands/RegenerateCommentContentCommandTest.php b/tests/Commands/RegenerateCommentContentCommandTest.php index 1deeaa703..08f137777 100644 --- a/tests/Commands/RegenerateCommentContentCommandTest.php +++ b/tests/Commands/RegenerateCommentContentCommandTest.php @@ -1,4 +1,6 @@ - "

    some_fresh_content

    \n", ]); } -} \ No newline at end of file +} diff --git a/tests/Commands/RegeneratePermissionsCommandTest.php b/tests/Commands/RegeneratePermissionsCommandTest.php index d5b34ba17..2090c991b 100644 --- a/tests/Commands/RegeneratePermissionsCommandTest.php +++ b/tests/Commands/RegeneratePermissionsCommandTest.php @@ -1,4 +1,6 @@ -assertDatabaseHas('joint_permissions', ['entity_id' => $page->id]); } -} \ No newline at end of file +} diff --git a/tests/Commands/ResetMfaCommandTest.php b/tests/Commands/ResetMfaCommandTest.php new file mode 100644 index 000000000..e65a048ef --- /dev/null +++ b/tests/Commands/ResetMfaCommandTest.php @@ -0,0 +1,65 @@ +artisan('bookstack:reset-mfa') + ->expectsOutput('Either a --id= or --email= option must be provided.') + ->assertExitCode(1); + } + + public function test_command_runs_with_provided_email() + { + /** @var User $user */ + $user = User::query()->first(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + + $this->assertEquals(1, $user->mfaValues()->count()); + $this->artisan("bookstack:reset-mfa --email={$user->email}") + ->expectsQuestion('Are you sure you want to proceed?', true) + ->expectsOutput('User MFA methods have been reset.') + ->assertExitCode(0); + $this->assertEquals(0, $user->mfaValues()->count()); + } + + public function test_command_runs_with_provided_id() + { + /** @var User $user */ + $user = User::query()->first(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + + $this->assertEquals(1, $user->mfaValues()->count()); + $this->artisan("bookstack:reset-mfa --id={$user->id}") + ->expectsQuestion('Are you sure you want to proceed?', true) + ->expectsOutput('User MFA methods have been reset.') + ->assertExitCode(0); + $this->assertEquals(0, $user->mfaValues()->count()); + } + + public function test_saying_no_to_confirmation_does_not_reset_mfa() + { + /** @var User $user */ + $user = User::query()->first(); + MfaValue::upsertWithValue($user, MfaValue::METHOD_TOTP, 'test'); + + $this->assertEquals(1, $user->mfaValues()->count()); + $this->artisan("bookstack:reset-mfa --id={$user->id}") + ->expectsQuestion('Are you sure you want to proceed?', false) + ->assertExitCode(1); + $this->assertEquals(1, $user->mfaValues()->count()); + } + + public function test_giving_non_existing_user_shows_error_message() + { + $this->artisan('bookstack:reset-mfa --email=donkeys@example.com') + ->expectsOutput('A user where email=donkeys@example.com could not be found.') + ->assertExitCode(1); + } +} diff --git a/tests/Commands/UpdateUrlCommandTest.php b/tests/Commands/UpdateUrlCommandTest.php index 7043ce047..0acccd80c 100644 --- a/tests/Commands/UpdateUrlCommandTest.php +++ b/tests/Commands/UpdateUrlCommandTest.php @@ -1,4 +1,6 @@ -artisan('bookstack:update-url https://example.com https://cats.example.com') ->expectsQuestion("This will search for \"https://example.com\" in your database and replace it with \"https://cats.example.com\".\nAre you sure you want to proceed?", 'y') - ->expectsQuestion("This operation could cause issues if used incorrectly. Have you made a backup of your existing database?", 'y'); + ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y'); $this->assertDatabaseHas('pages', [ - 'id' => $page->id, - 'html' => '' + 'id' => $page->id, + 'html' => '', ]); } public function test_command_requires_valid_url() { - $badUrlMessage = "The given urls are expected to be full urls starting with http:// or https://"; + $badUrlMessage = 'The given urls are expected to be full urls starting with http:// or https://'; $this->artisan('bookstack:update-url //example.com https://cats.example.com')->expectsOutput($badUrlMessage); $this->artisan('bookstack:update-url https://example.com htts://cats.example.com')->expectsOutput($badUrlMessage); $this->artisan('bookstack:update-url example.com https://cats.example.com')->expectsOutput($badUrlMessage); @@ -54,6 +56,6 @@ class UpdateUrlCommandTest extends TestCase { $this->artisan("bookstack:update-url {$oldUrl} {$newUrl}") ->expectsQuestion("This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\nAre you sure you want to proceed?", 'y') - ->expectsQuestion("This operation could cause issues if used incorrectly. Have you made a backup of your existing database?", 'y'); + ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y'); } -} \ No newline at end of file +} diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php index 42a5da2d1..b1cefbb65 100644 --- a/tests/CreatesApplication.php +++ b/tests/CreatesApplication.php @@ -1,4 +1,6 @@ -make(Kernel::class)->bootstrap(); + return $app; } -} \ No newline at end of file +} diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index 60658f6b2..480d29002 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -1,4 +1,6 @@ -get(); $shelfInfo = [ - 'name' => 'My test book' . Str::random(4), - 'description' => 'Test book description ' . Str::random(10) + 'name' => 'My test book' . Str::random(4), + 'description' => 'Test book description ' . Str::random(10), ]; $resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [ 'books' => $booksToInclude->implode('id', ','), - 'tags' => [ + 'tags' => [ [ - 'name' => 'Test Category', + 'name' => 'Test Category', 'value' => 'Test Tag Value', - ] + ], ], ])); $resp->assertRedirect(); @@ -109,8 +110,8 @@ class BookShelfTest extends TestCase public function test_shelves_create_sets_cover_image() { $shelfInfo = [ - 'name' => 'My test book' . Str::random(4), - 'description' => 'Test book description ' . Str::random(10) + 'name' => 'My test book' . Str::random(4), + 'description' => 'Test book description ' . Str::random(10), ]; $imageFile = $this->getTestImage('shelf-test.png'); @@ -120,7 +121,7 @@ class BookShelfTest extends TestCase $lastImage = Image::query()->orderByDesc('id')->firstOrFail(); $shelf = Bookshelf::query()->where('name', '=', $shelfInfo['name'])->first(); $this->assertDatabaseHas('bookshelves', [ - 'id' => $shelf->id, + 'id' => $shelf->id, 'image_id' => $lastImage->id, ]); $this->assertEquals($lastImage->id, $shelf->cover->id); @@ -175,7 +176,7 @@ class BookShelfTest extends TestCase // Set book ordering $this->asAdmin()->put($shelf->getUrl(), [ 'books' => $books->implode('id', ','), - 'tags' => [], 'description' => 'abc', 'name' => 'abc' + 'tags' => [], 'description' => 'abc', 'name' => 'abc', ]); $this->assertEquals(3, $shelf->books()->count()); $shelf->refresh(); @@ -205,17 +206,17 @@ class BookShelfTest extends TestCase $booksToInclude = Book::take(2)->get(); $shelfInfo = [ - 'name' => 'My test book' . Str::random(4), - 'description' => 'Test book description ' . Str::random(10) + 'name' => 'My test book' . Str::random(4), + 'description' => 'Test book description ' . Str::random(10), ]; $resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [ 'books' => $booksToInclude->implode('id', ','), - 'tags' => [ + 'tags' => [ [ - 'name' => 'Test Category', + 'name' => 'Test Category', 'value' => 'Test Tag Value', - ] + ], ], ])); $shelf = Bookshelf::find($shelf->id); @@ -246,15 +247,15 @@ class BookShelfTest extends TestCase $testName = 'Test Book in Shelf Name'; $createBookResp = $this->asEditor()->post($shelf->getUrl('/create-book'), [ - 'name' => $testName, - 'description' => 'Book in shelf description' + 'name' => $testName, + 'description' => 'Book in shelf description', ]); $createBookResp->assertRedirect(); $newBook = Book::query()->orderBy('id', 'desc')->first(); $this->assertDatabaseHas('bookshelves_books', [ 'bookshelf_id' => $shelf->id, - 'book_id' => $newBook->id, + 'book_id' => $newBook->id, ]); $resp = $this->asEditor()->get($shelf->getUrl()); @@ -293,20 +294,27 @@ class BookShelfTest extends TestCase $child = $shelf->books()->first(); $editorRole = $this->getEditor()->roles()->first(); - $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default"); - $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default"); + $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default'); + $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default'); $this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]); $resp = $this->post($shelf->getUrl('/copy-permissions')); $child = $shelf->books()->first(); $resp->assertRedirect($shelf->getUrl()); - $this->assertTrue(boolval($child->restricted), "Child book should now be restricted"); - $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions"); + $this->assertTrue(boolval($child->restricted), 'Child book should now be restricted'); + $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions'); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]); } + public function test_permission_page_has_a_warning_about_no_cascading() + { + $shelf = Bookshelf::first(); + $resp = $this->asAdmin()->get($shelf->getUrl('/permissions')); + $resp->assertSeeText('Permissions on bookshelves do not automatically cascade to contained books.'); + } + public function test_bookshelves_show_in_breadcrumbs_if_in_context() { $shelf = Bookshelf::first(); @@ -337,8 +345,8 @@ class BookShelfTest extends TestCase { // Create shelf $shelfInfo = [ - 'name' => 'My test shelf' . Str::random(4), - 'description' => 'Test shelf description ' . Str::random(10) + 'name' => 'My test shelf' . Str::random(4), + 'description' => 'Test shelf description ' . Str::random(10), ]; $this->asEditor()->post('/shelves', $shelfInfo); @@ -346,8 +354,8 @@ class BookShelfTest extends TestCase // Create book and add to shelf $this->asEditor()->post($shelf->getUrl('/create-book'), [ - 'name' => 'Test book name', - 'description' => 'Book in shelf description' + 'name' => 'Test book name', + 'description' => 'Book in shelf description', ]); $newBook = Book::query()->orderBy('id', 'desc')->first(); diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index e1b06431b..b4ba2fa82 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -1,4 +1,6 @@ -assertElementContains('#sibling-navigation', 'Previous'); $resp->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20)); } -} \ No newline at end of file +} diff --git a/tests/Entity/ChapterTest.php b/tests/Entity/ChapterTest.php index e9350a32b..45c132e89 100644 --- a/tests/Entity/ChapterTest.php +++ b/tests/Entity/ChapterTest.php @@ -1,4 +1,6 @@ -get($deleteReq->baseResponse->headers->get('location')); $redirectReq->assertNotificationContains('Chapter Successfully Deleted'); } -} \ No newline at end of file +} diff --git a/tests/Entity/CommentSettingTest.php b/tests/Entity/CommentSettingTest.php index 5ab6ad9a8..d8caa7358 100644 --- a/tests/Entity/CommentSettingTest.php +++ b/tests/Entity/CommentSettingTest.php @@ -1,4 +1,6 @@ -asAdmin()->get($this->page->getUrl()) ->assertElementExists('.comments-list'); } -} \ No newline at end of file +} diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 63d1a29a2..3bf51556e 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -1,12 +1,13 @@ -asAdmin(); @@ -22,11 +23,11 @@ class CommentTest extends TestCase $pageResp->assertSee($comment->text); $this->assertDatabaseHas('comments', [ - 'local_id' => 1, - 'entity_id' => $page->id, + 'local_id' => 1, + 'entity_id' => $page->id, 'entity_type' => Page::newModelInstance()->getMorphClass(), - 'text' => $comment->text, - 'parent_id' => 2 + 'text' => $comment->text, + 'parent_id' => 2, ]); } @@ -49,8 +50,8 @@ class CommentTest extends TestCase $resp->assertDontSee($comment->text); $this->assertDatabaseHas('comments', [ - 'text' => $newText, - 'entity_id' => $page->id + 'text' => $newText, + 'entity_id' => $page->id, ]); } @@ -68,7 +69,7 @@ class CommentTest extends TestCase $resp->assertStatus(200); $this->assertDatabaseMissing('comments', [ - 'id' => $comment->id + 'id' => $comment->id, ]); } @@ -80,10 +81,10 @@ class CommentTest extends TestCase ]); $this->assertDatabaseHas('comments', [ - 'entity_id' => $page->id, + 'entity_id' => $page->id, 'entity_type' => $page->getMorphClass(), - 'text' => '# My Title', - 'html' => "

    My Title

    \n", + 'text' => '# My Title', + 'html' => "

    My Title

    \n", ]); $pageView = $this->get($page->getUrl()); diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index 6b53b2cb6..8d2ef0fde 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -1,4 +1,6 @@ -first(); @@ -80,13 +81,13 @@ class EntitySearchTest extends TestCase { $newTags = [ new Tag([ - 'name' => 'animal', - 'value' => 'cat' + 'name' => 'animal', + 'value' => 'cat', ]), new Tag([ - 'name' => 'color', - 'value' => 'red' - ]) + 'name' => 'color', + 'value' => 'red', + ]), ]; $pageA = Page::first(); @@ -135,22 +136,22 @@ class EntitySearchTest extends TestCase $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name); $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name); $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name); - $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorSlug.'}'))->assertDontSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertDontSee($page->name); $page->created_by = $editorId; $page->save(); $this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name); - $this->get('/search?term=' . urlencode('danzorbhsing {created_by: '.$editorSlug.'}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editorSlug . '}'))->assertSee($page->name); $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name); $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name); $page->updated_by = $editorId; $page->save(); $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name); - $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorSlug.'}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertSee($page->name); $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name); $page->owned_by = $editorId; $page->save(); $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name); - $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:'.$editorSlug.'}'))->assertSee($page->name); + $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editorSlug . '}'))->assertSee($page->name); // Content filters $this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name); diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php index 52f9a3ae2..f8c88b1fe 100644 --- a/tests/Entity/EntityTest.php +++ b/tests/Entity/EntityTest.php @@ -1,17 +1,18 @@ -asAdmin() ->visit($bookToSort->getUrl()) @@ -54,7 +55,7 @@ class EntityTest extends BrowserKitTest public function test_book_sort_item_returns_book_content() { - $books = Book::all(); + $books = Book::all(); $bookToSort = $books[0]; $firstPage = $bookToSort->pages[0]; $firstChapter = $bookToSort->chapters[0]; @@ -84,13 +85,12 @@ class EntityTest extends BrowserKitTest ->submitForm('Grid View') ->seePageIs('/books') ->pageHasElement('.featured-image-container'); - } public function pageCreation($chapter) { $page = factory(Page::class)->make([ - 'name' => 'My First Page' + 'name' => 'My First Page', ]); $this->asAdmin() @@ -110,13 +110,14 @@ class EntityTest extends BrowserKitTest ->see($page->name); $page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first(); + return $page; } public function chapterCreation(Book $book) { $chapter = factory(Chapter::class)->make([ - 'name' => 'My First Chapter' + 'name' => 'My First Chapter', ]); $this->asAdmin() @@ -133,13 +134,14 @@ class EntityTest extends BrowserKitTest ->see($chapter->name)->see($chapter->description); $chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first(); + return $chapter; } public function bookCreation() { $book = factory(Book::class)->make([ - 'name' => 'My First Book' + 'name' => 'My First Book', ]); $this->asAdmin() ->visit('/books') @@ -165,6 +167,7 @@ class EntityTest extends BrowserKitTest $this->assertMatchesRegularExpression($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); $book = Book::where('slug', '=', 'my-first-book')->first(); + return $book; } @@ -245,7 +248,7 @@ class EntityTest extends BrowserKitTest { $page = Page::orderBy('updated_at', 'asc')->first(); Page::where('id', '!=', $page->id)->update([ - 'updated_at' => Carbon::now()->subSecond(1) + 'updated_at' => Carbon::now()->subSecond(1), ]); $this->asAdmin()->visit('/') ->dontSeeInElement('#recently-updated-pages', $page->name); @@ -258,13 +261,13 @@ class EntityTest extends BrowserKitTest public function test_slug_multi_byte_url_safe() { $book = $this->newBook([ - 'name' => 'информация' + 'name' => 'информация', ]); $this->assertEquals('informatsiya', $book->slug); $book = $this->newBook([ - 'name' => '¿Qué?' + 'name' => '¿Qué?', ]); $this->assertEquals('que', $book->slug); @@ -273,7 +276,7 @@ class EntityTest extends BrowserKitTest public function test_slug_format() { $book = $this->newBook([ - 'name' => 'PartA / PartB / PartC' + 'name' => 'PartA / PartB / PartC', ]); $this->assertEquals('parta-partb-partc', $book->slug); @@ -313,5 +316,4 @@ class EntityTest extends BrowserKitTest ->submitForm('Confirm') ->seePageIs($chapter->getUrl()); } - } diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index f9ba3d90e..aebc5f245 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -1,5 +1,8 @@ -first(); @@ -133,7 +135,7 @@ class ExportTest extends TestCase { $page = Page::query()->first(); - $customHeadContent = ""; + $customHeadContent = ''; $this->setSettings(['app-custom-head' => $customHeadContent]); $resp = $this->asEditor()->get($page->getUrl('/export/html')); @@ -144,7 +146,7 @@ class ExportTest extends TestCase { $page = Page::query()->first(); - $customHeadContent = ""; + $customHeadContent = ''; $this->setSettings(['app-custom-head' => $customHeadContent]); $resp = $this->asEditor()->get($page->getUrl('/export/html')); @@ -209,8 +211,8 @@ class ExportTest extends TestCase { $page = Page::query()->first(); $page->html = '' - .'' - .''; + . '' + . ''; $storageDisk = Storage::disk('local'); $storageDisk->makeDirectory('uploads/images/gallery'); $storageDisk->put('uploads/images/gallery/svg_test.svg', 'good'); @@ -259,4 +261,125 @@ class ExportTest extends TestCase $resp->assertDontSee('ExportWizardTheFifth'); } + public function test_page_markdown_export() + { + $page = Page::query()->first(); + + $resp = $this->asEditor()->get($page->getUrl('/export/markdown')); + $resp->assertStatus(200); + $resp->assertSee($page->name); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"'); + } + + public function test_page_markdown_export_uses_existing_markdown_if_apparent() + { + $page = Page::query()->first()->forceFill([ + 'markdown' => '# A header', + 'html' => '

    Dogcat

    ', + ]); + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl('/export/markdown')); + $resp->assertSee('A header'); + $resp->assertDontSee('Dogcat'); + } + + public function test_page_markdown_export_converts_html_where_no_markdown() + { + $page = Page::query()->first()->forceFill([ + 'markdown' => '', + 'html' => '

    Dogcat

    Some bold text

    ', + ]); + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl('/export/markdown')); + $resp->assertSee("# Dogcat\n\nSome **bold** text"); + } + + public function test_page_markdown_export_does_not_convert_callouts() + { + $page = Page::query()->first()->forceFill([ + 'markdown' => '', + 'html' => '

    Dogcat

    Some callout text

    Another line

    ', + ]); + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl('/export/markdown')); + $resp->assertSee("# Dogcat\n\n

    Some callout text

    \n\nAnother line"); + } + + public function test_page_markdown_export_handles_bookstacks_wysiwyg_codeblock_format() + { + $page = Page::query()->first()->forceFill([ + 'markdown' => '', + 'html' => '

    Dogcat

    ' . "\r\n" . '
    var a = \'cat\';

    Another line

    ', + ]); + $page->save(); + + $resp = $this->asEditor()->get($page->getUrl('/export/markdown')); + $resp->assertSee("# Dogcat\n\n```JavaScript\nvar a = 'cat';\n```\n\nAnother line"); + } + + public function test_chapter_markdown_export() + { + $chapter = Chapter::query()->first(); + $page = $chapter->pages()->first(); + $resp = $this->asEditor()->get($chapter->getUrl('/export/markdown')); + + $resp->assertSee('# ' . $chapter->name); + $resp->assertSee('# ' . $page->name); + } + + public function test_book_markdown_export() + { + $book = Book::query()->whereHas('pages')->whereHas('chapters')->first(); + $chapter = $book->chapters()->first(); + $page = $chapter->pages()->first(); + $resp = $this->asEditor()->get($book->getUrl('/export/markdown')); + + $resp->assertSee('# ' . $book->name); + $resp->assertSee('# ' . $chapter->name); + $resp->assertSee('# ' . $page->name); + } + + public function test_export_option_only_visible_and_accessible_with_permission() + { + $book = Book::query()->whereHas('pages')->whereHas('chapters')->first(); + $chapter = $book->chapters()->first(); + $page = $chapter->pages()->first(); + $entities = [$book, $chapter, $page]; + $user = $this->getViewer(); + $this->actingAs($user); + + foreach ($entities as $entity) { + $resp = $this->get($entity->getUrl()); + $resp->assertSee('/export/pdf'); + } + + /** @var Role $role */ + $this->removePermissionFromUser($user, 'content-export'); + + foreach ($entities as $entity) { + $resp = $this->get($entity->getUrl()); + $resp->assertDontSee('/export/pdf'); + $resp = $this->get($entity->getUrl('/export/pdf')); + $this->assertPermissionError($resp); + } + } + + public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true() + { + /** @var Page $page */ + $page = Page::query()->first(); + + config()->set('snappy.pdf.binary', '/abc123'); + config()->set('app.allow_untrusted_server_fetching', false); + + $resp = $this->asEditor()->get($page->getUrl('/export/pdf')); + $resp->assertStatus(200); // Sucessful response with invalid snappy binary indicates dompdf usage. + + config()->set('app.allow_untrusted_server_fetching', true); + $resp = $this->get($page->getUrl('/export/pdf')); + $resp->assertStatus(500); // Bad response indicates wkhtml usage + } } diff --git a/tests/Entity/MarkdownTest.php b/tests/Entity/MarkdownTest.php index 5e5fa8a0c..7de7ea11b 100644 --- a/tests/Entity/MarkdownTest.php +++ b/tests/Entity/MarkdownTest.php @@ -1,4 +1,6 @@ -asAdmin()->visit($this->page->getUrl() . '/edit') ->pageHasElement('#html-editor'); } - + public function test_markdown_setting_shows_markdown_editor() { $this->setMarkdownEditor(); @@ -48,5 +50,4 @@ class MarkdownTest extends BrowserKitTest $this->asAdmin()->visit($this->page->getUrl() . '/edit') ->seeInField('markdown', $this->page->html); } - -} \ No newline at end of file +} diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index f1462dbd0..5aee97887 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -1,7 +1,9 @@ -assertElementNotContains('.page-content', ''); } - } public function test_iframe_js_and_base64_urls_are_removed() @@ -141,7 +142,7 @@ class PageContentTest extends TestCase '', '', '', - '' + '', ]; $this->asEditor(); @@ -160,14 +161,13 @@ class PageContentTest extends TestCase $pageView->assertElementNotContains('.page-content', 'data:'); $pageView->assertElementNotContains('.page-content', 'base64'); } - } public function test_javascript_uri_links_are_removed() { $checks = [ '', '
    ', - '
    ' + '
    ', ]; $this->asEditor(); @@ -207,7 +208,7 @@ class PageContentTest extends TestCase $pageView->assertElementNotContains('.page-content', 'formaction=javascript:'); } } - + public function test_metadata_redirects_are_removed() { $checks = [ @@ -229,6 +230,7 @@ class PageContentTest extends TestCase $pageView->assertElementNotContains('.page-content', 'external_url'); } } + public function test_page_inline_on_attributes_removed_by_default() { $this->asEditor(); @@ -265,7 +267,6 @@ class PageContentTest extends TestCase $pageView->assertStatus(200); $pageView->assertElementNotContains('.page-content', 'onclick'); } - } public function test_page_content_scripts_show_when_configured() @@ -308,7 +309,7 @@ class PageContentTest extends TestCase $pageA->html = $content; $pageA->save(); - $pageB->html = '

      {{@'. $pageA->id .'#test}}

      '; + $pageB->html = '

        {{@' . $pageA->id . '#test}}

        '; $pageB->save(); $pageView = $this->get($pageB->getUrl()); @@ -322,14 +323,14 @@ class PageContentTest extends TestCase $content = '
        • test a
          • test b
        '; $pageSave = $this->put($page->getUrl(), [ - 'name' => $page->name, - 'html' => $content, - 'summary' => '' + 'name' => $page->name, + 'html' => $content, + 'summary' => '', ]); $pageSave->assertRedirect(); $updatedPage = Page::query()->where('id', '=', $page->id)->first(); - $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1); + $this->assertEquals(substr_count($updatedPage->html, 'bkmrk-test"'), 1); } public function test_anchors_referencing_non_bkmrk_ids_rewritten_after_save() @@ -339,9 +340,9 @@ class PageContentTest extends TestCase $content = '

        test

        link

        '; $this->put($page->getUrl(), [ - 'name' => $page->name, - 'html' => $content, - 'summary' => '' + 'name' => $page->name, + 'html' => $content, + 'summary' => '', ]); $updatedPage = Page::query()->where('id', '=', $page->id)->first(); @@ -358,21 +359,21 @@ class PageContentTest extends TestCase $this->assertCount(3, $navMap); $this->assertArrayMapIncludes([ 'nodeName' => 'h1', - 'link' => '#testa', - 'text' => 'Hello', - 'level' => 1, + 'link' => '#testa', + 'text' => 'Hello', + 'level' => 1, ], $navMap[0]); $this->assertArrayMapIncludes([ 'nodeName' => 'h2', - 'link' => '#testb', - 'text' => 'There', - 'level' => 2, + 'link' => '#testb', + 'text' => 'There', + 'level' => 2, ], $navMap[1]); $this->assertArrayMapIncludes([ 'nodeName' => 'h3', - 'link' => '#testc', - 'text' => 'Donkey', - 'level' => 3, + 'link' => '#testc', + 'text' => 'Donkey', + 'level' => 3, ], $navMap[2]); } @@ -385,8 +386,8 @@ class PageContentTest extends TestCase $this->assertCount(1, $navMap); $this->assertArrayMapIncludes([ 'nodeName' => 'h1', - 'link' => '#testa', - 'text' => 'Hello' + 'link' => '#testa', + 'text' => 'Hello', ], $navMap[0]); } @@ -399,15 +400,15 @@ class PageContentTest extends TestCase $this->assertCount(3, $navMap); $this->assertArrayMapIncludes([ 'nodeName' => 'h4', - 'level' => 1, + 'level' => 1, ], $navMap[0]); $this->assertArrayMapIncludes([ 'nodeName' => 'h5', - 'level' => 2, + 'level' => 2, ], $navMap[1]); $this->assertArrayMapIncludes([ 'nodeName' => 'h6', - 'level' => 3, + 'level' => 3, ], $navMap[2]); } @@ -436,7 +437,7 @@ class PageContentTest extends TestCase | Paragraph | Text |'; $this->put($page->getUrl(), [ 'name' => $page->name, 'markdown' => $content, - 'html' => '', 'summary' => '' + 'html' => '', 'summary' => '', ]); $page->refresh(); @@ -455,7 +456,7 @@ class PageContentTest extends TestCase - [x] Item b'; $this->put($page->getUrl(), [ 'name' => $page->name, 'markdown' => $content, - 'html' => '', 'summary' => '' + 'html' => '', 'summary' => '', ]); $page->refresh(); @@ -463,7 +464,8 @@ class PageContentTest extends TestCase $this->assertStringContainsString('type="checkbox"', $page->html); $pageView = $this->get($page->getUrl()); - $pageView->assertElementExists('.page-content input[type=checkbox]'); + $pageView->assertElementExists('.page-content li.task-list-item input[type=checkbox]'); + $pageView->assertElementExists('.page-content li.task-list-item input[type=checkbox][checked=checked]'); } public function test_page_markdown_strikethrough_rendering() @@ -474,7 +476,7 @@ class PageContentTest extends TestCase $content = '~~some crossed out text~~'; $this->put($page->getUrl(), [ 'name' => $page->name, 'markdown' => $content, - 'html' => '', 'summary' => '' + 'html' => '', 'summary' => '', ]); $page->refresh(); @@ -492,7 +494,7 @@ class PageContentTest extends TestCase $content = ''; $this->put($page->getUrl(), [ 'name' => $page->name, 'markdown' => $content, - 'html' => '', 'summary' => '' + 'html' => '', 'summary' => '', ]); $page->refresh(); @@ -510,7 +512,7 @@ class PageContentTest extends TestCase $this->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', - 'html' => '

        test

        ', + 'html' => '

        test

        ', ]); $page->refresh(); @@ -534,7 +536,7 @@ class PageContentTest extends TestCase $base64PngWithoutWhitespace = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQAB'; $this->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', - 'html' => '

        test

        ', + 'html' => '

        test

        ', ]); $page->refresh(); @@ -556,7 +558,7 @@ class PageContentTest extends TestCase $this->put($page->getUrl(), [ 'name' => $page->name, 'summary' => '', - 'html' => '

        test

        ', + 'html' => '

        test

        ', ]); $page->refresh(); diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 0e3980c67..68059af6e 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -1,4 +1,6 @@ -actingAs($newUser) ->visit($this->page->getUrl('/edit')) ->see('Admin has started editing this page'); - $this->flushSession(); + $this->flushSession(); $this->visit($nonEditedPage->getUrl() . '/edit') ->dontSeeInElement('.notification', 'Admin has started editing this page'); } @@ -112,5 +114,4 @@ class PageDraftTest extends BrowserKitTest 'html' => $page->html, ]); } - } diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 62fbfbf31..2ed7d3b41 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -1,4 +1,6 @@ -update($page, ['name' => 'updated page abc123', 'html' => '

        new contente def456

        ', 'summary' => 'initial page revision testing']); $pageRepo->update($page, ['name' => 'updated page again', 'html' => '

        new content

        ', 'summary' => 'page revision testing']); - $page = Page::find($page->id); - + $page = Page::find($page->id); $pageView = $this->get($page->getUrl()); $pageView->assertDontSee('abc123'); @@ -56,7 +57,7 @@ class PageRevisionTest extends TestCase $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first(); $restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore'); - $page = Page::find($page->id); + $page = Page::find($page->id); $restoreReq->assertStatus(302); $restoreReq->assertRedirect($page->getUrl()); @@ -89,7 +90,7 @@ class PageRevisionTest extends TestCase $pageView = $this->get($page->getUrl()); $this->assertDatabaseHas('pages', [ - 'id' => $page->id, + 'id' => $page->id, 'markdown' => '## New Content def456', ]); $pageView->assertSee('abc123'); @@ -112,8 +113,8 @@ class PageRevisionTest extends TestCase $this->assertDatabaseHas('page_revisions', [ 'page_id' => $page->id, - 'text' => 'new contente def456', - 'type' => 'version', + 'text' => 'new contente def456', + 'type' => 'version', 'summary' => "Restored from #{$revToRestore->id}; My first update", ]); } @@ -125,7 +126,7 @@ class PageRevisionTest extends TestCase $resp = $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); $resp->assertStatus(302); - $this->assertTrue(Page::find($page->id)->revision_count === $startCount+1); + $this->assertTrue(Page::find($page->id)->revision_count === $startCount + 1); } public function test_revision_count_shown_in_page_meta() @@ -141,7 +142,8 @@ class PageRevisionTest extends TestCase $pageView->assertSee('Revision #' . $page->revision_count); } - public function test_revision_deletion() { + public function test_revision_deletion() + { $page = Page::first(); $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']); @@ -201,4 +203,4 @@ class PageRevisionTest extends TestCase $revisionCount = $page->revisions()->count(); $this->assertEquals(12, $revisionCount); } -} \ No newline at end of file +} diff --git a/tests/Entity/PageTemplateTest.php b/tests/Entity/PageTemplateTest.php index a5594e8b8..3d1689510 100644 --- a/tests/Entity/PageTemplateTest.php +++ b/tests/Entity/PageTemplateTest.php @@ -1,4 +1,6 @@ -actingAs($editor); $pageUpdateData = [ - 'name' => $page->name, - 'html' => $page->html, + 'name' => $page->name, + 'html' => $page->html, 'template' => 'true', ]; $this->put($page->getUrl(), $pageUpdateData); $this->assertDatabaseHas('pages', [ - 'id' => $page->id, + 'id' => $page->id, 'template' => false, ]); @@ -42,7 +44,7 @@ class PageTemplateTest extends TestCase $this->put($page->getUrl(), $pageUpdateData); $this->assertDatabaseHas('pages', [ - 'id' => $page->id, + 'id' => $page->id, 'template' => true, ]); } @@ -64,7 +66,7 @@ class PageTemplateTest extends TestCase $templateFetch = $this->get('/templates/' . $page->id); $templateFetch->assertStatus(200); $templateFetch->assertJson([ - 'html' => $content, + 'html' => $content, 'markdown' => '', ]); } @@ -86,5 +88,4 @@ class PageTemplateTest extends TestCase $templatesFetch->assertSee($page->name); $templatesFetch->assertSee('pagination'); } - -} \ No newline at end of file +} diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index a6f6f9d50..2721c225c 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -1,4 +1,6 @@ -first(); @@ -33,22 +34,22 @@ class PageTest extends TestCase $details = [ 'markdown' => '# a title', - 'html' => '

        a title

        ', - 'name' => 'my page', + 'html' => '

        a title

        ', + 'name' => 'my page', ]; $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details); $resp->assertRedirect(); $this->assertDatabaseHas('pages', [ 'markdown' => $details['markdown'], - 'name' => $details['name'], - 'id' => $draft->id, - 'draft' => false + 'name' => $details['name'], + 'id' => $draft->id, + 'draft' => false, ]); $draft->refresh(); - $resp = $this->get($draft->getUrl("/edit")); - $resp->assertSee("# a title"); + $resp = $this->get($draft->getUrl('/edit')); + $resp->assertSee('# a title'); } public function test_page_delete() @@ -112,7 +113,7 @@ class PageTest extends TestCase $movePageResp = $this->post($page->getUrl('/copy'), [ 'entity_selection' => 'book:' . $newBook->id, - 'name' => 'My copied test page' + 'name' => 'My copied test page', ]); $pageCopy = Page::where('name', '=', 'My copied test page')->first(); @@ -131,7 +132,7 @@ class PageTest extends TestCase $this->asEditor()->post($page->getUrl('/copy'), [ 'entity_selection' => 'book:' . $newBook->id, - 'name' => 'My copied test page' + 'name' => 'My copied test page', ]); $pageCopy = Page::where('name', '=', 'My copied test page')->first(); @@ -148,7 +149,7 @@ class PageTest extends TestCase $resp->assertSee('Copy Page'); $movePageResp = $this->post($page->getUrl('/copy'), [ - 'name' => 'My copied test page' + 'name' => 'My copied test page', ]); $pageCopy = Page::where('name', '=', 'My copied test page')->first(); @@ -178,14 +179,14 @@ class PageTest extends TestCase $movePageResp = $this->post($page->getUrl('/copy'), [ 'entity_selection' => 'book:' . $newBook->id, - 'name' => 'My copied test page' + 'name' => 'My copied test page', ]); $movePageResp->assertRedirect(); $this->assertDatabaseHas('pages', [ - 'name' => 'My copied test page', + 'name' => 'My copied test page', 'created_by' => $viewer->id, - 'book_id' => $newBook->id, + 'book_id' => $newBook->id, ]); } @@ -199,7 +200,7 @@ class PageTest extends TestCase ->where('draft', '=', true)->first(); $details = [ - 'name' => 'my page', + 'name' => 'my page', 'markdown' => '', ]; $resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details); @@ -207,8 +208,8 @@ class PageTest extends TestCase $this->assertDatabaseHas('pages', [ 'markdown' => $details['markdown'], - 'id' => $draft->id, - 'draft' => false + 'id' => $draft->id, + 'draft' => false, ]); } -} \ No newline at end of file +} diff --git a/tests/Entity/SearchOptionsTest.php b/tests/Entity/SearchOptionsTest.php index c9e116523..25a0ae720 100644 --- a/tests/Entity/SearchOptionsTest.php +++ b/tests/Entity/SearchOptionsTest.php @@ -1,4 +1,6 @@ -searches = ['cat']; $options->exacts = ['dog']; $options->tags = ['tag=good']; @@ -36,8 +38,8 @@ class SearchOptionsTest extends TestCase $this->assertEquals([ 'is_tree' => '', - 'name' => 'dan', - 'cat' => 'happy', + 'name' => 'dan', + 'cat' => 'happy', ], $opts->filters); } } diff --git a/tests/Entity/SortTest.php b/tests/Entity/SortTest.php index c27c41f29..f3d50b67d 100644 --- a/tests/Entity/SortTest.php +++ b/tests/Entity/SortTest.php @@ -1,4 +1,6 @@ -assertSee('Move Page'); $movePageResp = $this->put($page->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $page = Page::find($page->id); @@ -59,7 +61,7 @@ class SortTest extends TestCase $newChapter = $newBook->chapters()->first(); $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ - 'entity_selection' => 'chapter:' . $newChapter->id + 'entity_selection' => 'chapter:' . $newChapter->id, ]); $page = Page::find($page->id); @@ -77,7 +79,7 @@ class SortTest extends TestCase $newBook = Book::where('id', '!=', $oldChapter->book_id)->first(); $movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $page->refresh(); @@ -99,13 +101,13 @@ class SortTest extends TestCase $this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], $editor->roles->all()); $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($movePageResp); $this->setEntityRestrictions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all()); $movePageResp = $this->put($page->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $page = Page::find($page->id); @@ -125,7 +127,7 @@ class SortTest extends TestCase $this->setEntityRestrictions($page, ['view', 'update', 'create'], $editor->roles->all()); $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($movePageResp); $pageView = $this->get($page->getUrl()); @@ -133,7 +135,7 @@ class SortTest extends TestCase $this->setEntityRestrictions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $movePageResp = $this->put($page->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $page = Page::find($page->id); @@ -152,7 +154,7 @@ class SortTest extends TestCase $chapterMoveResp->assertSee('Move Chapter'); $moveChapterResp = $this->put($chapter->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $chapter = Chapter::find($chapter->id); @@ -180,7 +182,7 @@ class SortTest extends TestCase $this->setEntityRestrictions($chapter, ['view', 'update', 'create'], $editor->roles->all()); $moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $this->assertPermissionError($moveChapterResp); $pageView = $this->get($chapter->getUrl()); @@ -188,7 +190,7 @@ class SortTest extends TestCase $this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all()); $moveChapterResp = $this->put($chapter->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $chapter = Chapter::find($chapter->id); @@ -207,7 +209,7 @@ class SortTest extends TestCase $pageToCheck->delete(); $this->asEditor()->put($chapter->getUrl('/move'), [ - 'entity_selection' => 'book:' . $newBook->id + 'entity_selection' => 'book:' . $newBook->id, ]); $pageToCheck->refresh(); @@ -224,20 +226,20 @@ class SortTest extends TestCase // Create request data $reqData = [ [ - 'id' => $chapterToMove->id, - 'sort' => 0, + 'id' => $chapterToMove->id, + 'sort' => 0, 'parentChapter' => false, - 'type' => 'chapter', - 'book' => $newBook->id - ] + 'type' => 'chapter', + 'book' => $newBook->id, + ], ]; foreach ($pagesToMove as $index => $page) { $reqData[] = [ - 'id' => $page->id, - 'sort' => $index, + 'id' => $page->id, + 'sort' => $index, 'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false, - 'type' => 'page', - 'book' => $newBook->id + 'type' => 'page', + 'book' => $newBook->id, ]; } @@ -245,9 +247,9 @@ class SortTest extends TestCase $sortResp->assertRedirect($newBook->getUrl()); $sortResp->assertStatus(302); $this->assertDatabaseHas('chapters', [ - 'id' => $chapterToMove->id, - 'book_id' => $newBook->id, - 'priority' => 0 + 'id' => $chapterToMove->id, + 'book_id' => $newBook->id, + 'priority' => 0, ]); $this->assertTrue($newBook->chapters()->count() === 1); $this->assertTrue($newBook->chapters()->first()->pages()->count() === 1); @@ -256,5 +258,4 @@ class SortTest extends TestCase $checkResp = $this->get(Page::find($checkPage->id)->getUrl()); $checkResp->assertSee($newBook->name); } - -} \ No newline at end of file +} diff --git a/tests/Entity/TagTest.php b/tests/Entity/TagTest.php index 62ebf5f1b..74da37f4a 100644 --- a/tests/Entity/TagTest.php +++ b/tests/Entity/TagTest.php @@ -1,4 +1,6 @@ -tags()->saveMany($tags); + return $entity; } @@ -89,12 +91,11 @@ class TagTest extends TestCase ]; $page = $this->getEntityWithTags(Page::class, $tags); - $resp = $this->asEditor()->get("/search?term=[category]"); + $resp = $this->asEditor()->get('/search?term=[category]'); $resp->assertSee($page->name); $resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'category'); $resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'buckets'); $resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'color'); $resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'red'); } - } diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 6b69355fc..2eeb6537e 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -1,11 +1,12 @@ -assertStatus(404); $resp->assertSeeText('Image Not Found'); } -} \ No newline at end of file +} diff --git a/tests/FavouriteTest.php b/tests/FavouriteTest.php index 7209063c9..a0f11886e 100644 --- a/tests/FavouriteTest.php +++ b/tests/FavouriteTest.php @@ -9,7 +9,6 @@ use Tests\TestCase; class FavouriteTest extends TestCase { - public function test_page_add_favourite_flow() { $page = Page::query()->first(); @@ -21,15 +20,15 @@ class FavouriteTest extends TestCase $resp = $this->post('/favourites/add', [ 'type' => get_class($page), - 'id' => $page->id, + 'id' => $page->id, ]); $resp->assertRedirect($page->getUrl()); $resp->assertSessionHas('success', "\"{$page->name}\" has been added to your favourites"); $this->assertDatabaseHas('favourites', [ - 'user_id' => $editor->id, + 'user_id' => $editor->id, 'favouritable_type' => $page->getMorphClass(), - 'favouritable_id' => $page->id, + 'favouritable_id' => $page->id, ]); } @@ -38,8 +37,8 @@ class FavouriteTest extends TestCase $page = Page::query()->first(); $editor = $this->getEditor(); Favourite::query()->forceCreate([ - 'user_id' => $editor->id, - 'favouritable_id' => $page->id, + 'user_id' => $editor->id, + 'favouritable_id' => $page->id, 'favouritable_type' => $page->getMorphClass(), ]); @@ -49,7 +48,7 @@ class FavouriteTest extends TestCase $resp = $this->post('/favourites/remove', [ 'type' => get_class($page), - 'id' => $page->id, + 'id' => $page->id, ]); $resp->assertRedirect($page->getUrl()); $resp->assertSessionHas('success', "\"{$page->name}\" has been removed from your favourites"); @@ -90,7 +89,7 @@ class FavouriteTest extends TestCase /** @var Page $page */ $page = Page::query()->first(); - $page->favourites()->save((new Favourite)->forceFill(['user_id' => $editor->id])); + $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id])); $resp = $this->get('/'); $resp->assertElementExists('#top-favourites'); @@ -106,7 +105,7 @@ class FavouriteTest extends TestCase $resp = $this->actingAs($editor)->get('/favourites'); $resp->assertDontSee($page->name); - $page->favourites()->save((new Favourite)->forceFill(['user_id' => $editor->id])); + $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id])); $resp = $this->get('/favourites'); $resp->assertSee($page->name); @@ -114,5 +113,4 @@ class FavouriteTest extends TestCase $resp = $this->get('/favourites?page=2'); $resp->assertDontSee($page->name); } - -} \ No newline at end of file +} diff --git a/tests/FooterLinksTest.php b/tests/FooterLinksTest.php index f0ff0c40d..cb2959411 100644 --- a/tests/FooterLinksTest.php +++ b/tests/FooterLinksTest.php @@ -4,10 +4,9 @@ use Tests\TestCase; class FooterLinksTest extends TestCase { - public function test_saving_setting() { - $resp = $this->asAdmin()->post("/settings", [ + $resp = $this->asAdmin()->post('/settings', [ 'setting-app-footer-links' => [ ['label' => 'My custom link 1', 'url' => 'https://example.com/1'], ['label' => 'My custom link 2', 'url' => 'https://example.com/2'], @@ -58,4 +57,4 @@ class FooterLinksTest extends TestCase $resp->assertElementContains('footer a[href="https://example.com/privacy"]', 'Privacy Policy'); $resp->assertElementContains('footer a[href="https://example.com/terms"]', 'Terms of Service'); } -} \ No newline at end of file +} diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php index a8e33465d..db4e94c6d 100644 --- a/tests/HomepageTest.php +++ b/tests/HomepageTest.php @@ -1,13 +1,13 @@ -asEditor(); @@ -42,8 +42,8 @@ class HomepageTest extends TestCase $content = str_repeat('This is the body content of my custom homepage.', 20); $customPage = $this->newPage(['name' => $name, 'html' => $content]); $this->setSettings([ - 'app-homepage' => $customPage->id, - 'app-homepage-type' => 'page' + 'app-homepage' => $customPage->id, + 'app-homepage-type' => 'page', ]); $homeVisit = $this->get('/'); @@ -68,8 +68,8 @@ class HomepageTest extends TestCase $content = str_repeat('This is the body content of my custom homepage.', 20); $customPage = $this->newPage(['name' => $name, 'html' => $content]); $this->setSettings([ - 'app-homepage' => $customPage->id, - 'app-homepage-type' => 'default' + 'app-homepage' => $customPage->id, + 'app-homepage-type' => 'default', ]); $pageDeleteReq = $this->delete($customPage->getUrl()); diff --git a/tests/LanguageTest.php b/tests/LanguageTest.php index d5c6e4532..a9070248e 100644 --- a/tests/LanguageTest.php +++ b/tests/LanguageTest.php @@ -1,8 +1,9 @@ -langs as $lang) { foreach ($files as $file) { $loadError = false; + try { $translations = trans(str_replace('.php', '', $file), [], $lang); } catch (\Exception $e) { @@ -74,10 +76,9 @@ class LanguageTest extends TestCase public function test_rtl_config_set_if_lang_is_rtl() { $this->asEditor(); - $this->assertFalse(config('app.rtl'), "App RTL config should be false by default"); + $this->assertFalse(config('app.rtl'), 'App RTL config should be false by default'); setting()->putUser($this->getEditor(), 'language', 'ar'); $this->get('/'); - $this->assertTrue(config('app.rtl'), "App RTL config should have been set to true by middleware"); + $this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware'); } - -} \ No newline at end of file +} diff --git a/tests/OpenGraphTest.php b/tests/OpenGraphTest.php new file mode 100644 index 000000000..17a5aa2c5 --- /dev/null +++ b/tests/OpenGraphTest.php @@ -0,0 +1,102 @@ +first(); + $resp = $this->asEditor()->get($page->getUrl()); + $tags = $this->getOpenGraphTags($resp); + + $this->assertEquals($page->getShortName() . ' | BookStack', $tags['title']); + $this->assertEquals($page->getUrl(), $tags['url']); + $this->assertEquals(Str::limit($page->text, 100, '...'), $tags['description']); + } + + public function test_chapter_tags() + { + $chapter = Chapter::query()->first(); + $resp = $this->asEditor()->get($chapter->getUrl()); + $tags = $this->getOpenGraphTags($resp); + + $this->assertEquals($chapter->getShortName() . ' | BookStack', $tags['title']); + $this->assertEquals($chapter->getUrl(), $tags['url']); + $this->assertEquals(Str::limit($chapter->description, 100, '...'), $tags['description']); + } + + public function test_book_tags() + { + $book = Book::query()->first(); + $resp = $this->asEditor()->get($book->getUrl()); + $tags = $this->getOpenGraphTags($resp); + + $this->assertEquals($book->getShortName() . ' | BookStack', $tags['title']); + $this->assertEquals($book->getUrl(), $tags['url']); + $this->assertEquals(Str::limit($book->description, 100, '...'), $tags['description']); + $this->assertArrayNotHasKey('image', $tags); + + // Test image set if image has cover image + $bookRepo = app(BookRepo::class); + $bookRepo->updateCoverImage($book, $this->getTestImage('image.png')); + $resp = $this->asEditor()->get($book->getUrl()); + $tags = $this->getOpenGraphTags($resp); + + $this->assertEquals($book->getBookCover(), $tags['image']); + } + + public function test_shelf_tags() + { + $shelf = Bookshelf::query()->first(); + $resp = $this->asEditor()->get($shelf->getUrl()); + $tags = $this->getOpenGraphTags($resp); + + $this->assertEquals($shelf->getShortName() . ' | BookStack', $tags['title']); + $this->assertEquals($shelf->getUrl(), $tags['url']); + $this->assertEquals(Str::limit($shelf->description, 100, '...'), $tags['description']); + $this->assertArrayNotHasKey('image', $tags); + + // Test image set if image has cover image + $shelfRepo = app(BookshelfRepo::class); + $shelfRepo->updateCoverImage($shelf, $this->getTestImage('image.png')); + $resp = $this->asEditor()->get($shelf->getUrl()); + $tags = $this->getOpenGraphTags($resp); + + $this->assertEquals($shelf->getBookCover(), $tags['image']); + } + + /** + * Parse the open graph tags from a test response. + */ + protected function getOpenGraphTags(TestResponse $resp): array + { + $tags = []; + + libxml_use_internal_errors(true); + $doc = new \DOMDocument(); + $doc->loadHTML($resp->getContent()); + $metaElems = $doc->getElementsByTagName('meta'); + /** @var \DOMElement $elem */ + foreach ($metaElems as $elem) { + $prop = $elem->getAttribute('property'); + $name = explode(':', $prop)[1] ?? null; + if ($name) { + $tags[$name] = $elem->getAttribute('content'); + } + } + + return $tags; + } +} diff --git a/tests/Permissions/EntityOwnerChangeTest.php b/tests/Permissions/EntityOwnerChangeTest.php index 2f06bff2e..fe508668e 100644 --- a/tests/Permissions/EntityOwnerChangeTest.php +++ b/tests/Permissions/EntityOwnerChangeTest.php @@ -1,16 +1,16 @@ -first(); @@ -46,5 +46,4 @@ class EntityOwnerChangeTest extends TestCase $this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]); $this->assertDatabaseHas('bookshelves', ['owned_by' => $user->id, 'id' => $shelf->id]); } - -} \ No newline at end of file +} diff --git a/tests/Permissions/EntityPermissionsTest.php b/tests/Permissions/EntityPermissionsTest.php index 8dc112e57..77c62fdb5 100644 --- a/tests/Permissions/EntityPermissionsTest.php +++ b/tests/Permissions/EntityPermissionsTest.php @@ -1,17 +1,18 @@ -setRestrictionsForTestRoles($chapter, ['view', 'create']); - $this->visit($chapterUrl . '/create-page') ->type('test page', 'name') ->type('test content', 'html') @@ -396,10 +396,10 @@ class EntityPermissionsTest extends BrowserKitTest ->press('Save Permissions') ->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true]) ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $shelf->id, + 'restrictable_id' => $shelf->id, 'restrictable_type' => Bookshelf::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'view' + 'role_id' => '2', + 'action' => 'view', ]); } @@ -413,10 +413,10 @@ class EntityPermissionsTest extends BrowserKitTest ->press('Save Permissions') ->seeInDatabase('books', ['id' => $book->id, 'restricted' => true]) ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $book->id, + 'restrictable_id' => $book->id, 'restrictable_type' => Book::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'view' + 'role_id' => '2', + 'action' => 'view', ]); } @@ -430,10 +430,10 @@ class EntityPermissionsTest extends BrowserKitTest ->press('Save Permissions') ->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true]) ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $chapter->id, + 'restrictable_id' => $chapter->id, 'restrictable_type' => Chapter::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'update' + 'role_id' => '2', + 'action' => 'update', ]); } @@ -447,10 +447,10 @@ class EntityPermissionsTest extends BrowserKitTest ->press('Save Permissions') ->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true]) ->seeInDatabase('entity_permissions', [ - 'restrictable_id' => $page->id, + 'restrictable_id' => $page->id, 'restrictable_type' => Page::newModelInstance()->getMorphClass(), - 'role_id' => '2', - 'action' => 'delete' + 'role_id' => '2', + 'action' => 'delete', ]); } @@ -679,7 +679,8 @@ class EntityPermissionsTest extends BrowserKitTest $this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort'); } - public function test_book_sort_permission() { + public function test_book_sort_permission() + { $firstBook = Book::first(); $secondBook = Book::find(2); @@ -692,12 +693,12 @@ class EntityPermissionsTest extends BrowserKitTest // Create request data $reqData = [ [ - 'id' => $firstBookChapter->id, - 'sort' => 0, + 'id' => $firstBookChapter->id, + 'sort' => 0, 'parentChapter' => false, - 'type' => 'chapter', - 'book' => $secondBook->id - ] + 'type' => 'chapter', + 'book' => $secondBook->id, + ], ]; // Move chapter from first book to a second book @@ -708,12 +709,12 @@ class EntityPermissionsTest extends BrowserKitTest $reqData = [ [ - 'id' => $secondBookChapter->id, - 'sort' => 0, + 'id' => $secondBookChapter->id, + 'sort' => 0, 'parentChapter' => false, - 'type' => 'chapter', - 'book' => $firstBook->id - ] + 'type' => 'chapter', + 'book' => $firstBook->id, + ], ]; // Move chapter from second book to first book diff --git a/tests/Permissions/ExportPermissionsTest.php b/tests/Permissions/ExportPermissionsTest.php index e5a1146a5..2e3d84fa1 100644 --- a/tests/Permissions/ExportPermissionsTest.php +++ b/tests/Permissions/ExportPermissionsTest.php @@ -1,4 +1,6 @@ -first(); @@ -63,5 +64,4 @@ class ExportPermissionsTest extends TestCase $resp->assertDontSee($pageContent); } } - -} \ No newline at end of file +} diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index 8398d0828..b9b1805b6 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -1,12 +1,14 @@ -type('Test Role', 'display_name') ->type('A little test description', 'description') ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc]) + ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc, 'mfa_enforced' => false]) ->seePageIs('/settings/roles'); // Updating $this->asAdmin()->visit('/settings/roles') ->see($testRoleDesc) ->click($testRoleName) ->type($testRoleUpdateName, '#display_name') + ->check('#mfa_enforced') ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc]) + ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc, 'mfa_enforced' => true]) ->seePageIs('/settings/roles'); // Deleting $this->asAdmin()->visit('/settings/roles') @@ -93,11 +96,11 @@ class RolesTest extends BrowserKitTest $editUrl = '/settings/users/' . $adminUser->id; $this->actingAs($adminUser)->put($editUrl, [ - 'name' => $adminUser->name, + 'name' => $adminUser->name, 'email' => $adminUser->email, 'roles' => [ 'viewer' => strval($viewerRole->id), - ] + ], ])->followRedirects(); $this->seePageIs($editUrl); @@ -134,7 +137,7 @@ class RolesTest extends BrowserKitTest public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision() { - $usersLink = 'href="'.url('/settings/users') . '"'; + $usersLink = 'href="' . url('/settings/users') . '"'; $this->actingAs($this->user)->visit('/')->dontSee($usersLink); $this->giveUserPermissions($this->user, ['users-manage']); $this->actingAs($this->user)->visit('/')->see($usersLink); @@ -152,13 +155,13 @@ class RolesTest extends BrowserKitTest ->assertResponseOk() ->seeElement('input[name=email][disabled]'); $this->put($userProfileUrl, [ - 'name' => 'my_new_name', + 'name' => 'my_new_name', 'email' => 'new_email@example.com', ]); $this->seeInDatabase('users', [ - 'id' => $this->user->id, + 'id' => $this->user->id, 'email' => $originalEmail, - 'name' => 'my_new_name', + 'name' => 'my_new_name', ]); $this->giveUserPermissions($this->user, ['users-manage']); @@ -168,14 +171,14 @@ class RolesTest extends BrowserKitTest ->dontSeeElement('input[name=email][disabled]') ->seeElement('input[name=email]'); $this->put($userProfileUrl, [ - 'name' => 'my_new_name_2', + 'name' => 'my_new_name_2', 'email' => 'new_email@example.com', ]); $this->seeInDatabase('users', [ - 'id' => $this->user->id, + 'id' => $this->user->id, 'email' => 'new_email@example.com', - 'name' => 'my_new_name_2', + 'name' => 'my_new_name_2', ]); } @@ -250,10 +253,11 @@ class RolesTest extends BrowserKitTest } /** - * Check a standard entity access permission + * Check a standard entity access permission. + * * @param string $permission - * @param array $accessUrls Urls that are only accessible after having the permission - * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission + * @param array $accessUrls Urls that are only accessible after having the permission + * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission */ private function checkAccessPermission($permission, $accessUrls = [], $visibles = []) { @@ -263,7 +267,7 @@ class RolesTest extends BrowserKitTest } foreach ($visibles as $url => $text) { $this->actingAs($this->user)->visit($url) - ->dontSeeInElement('.action-buttons',$text); + ->dontSeeInElement('.action-buttons', $text); } $this->giveUserPermissions($this->user, [$permission]); @@ -281,9 +285,9 @@ class RolesTest extends BrowserKitTest public function test_bookshelves_create_all_permissions() { $this->checkAccessPermission('bookshelf-create-all', [ - '/create-shelf' + '/create-shelf', ], [ - '/shelves' => 'New Shelf' + '/shelves' => 'New Shelf', ]); $this->visit('/create-shelf') @@ -301,9 +305,9 @@ class RolesTest extends BrowserKitTest $this->regenEntityPermissions($ownShelf); $this->checkAccessPermission('bookshelf-update-own', [ - $ownShelf->getUrl('/edit') + $ownShelf->getUrl('/edit'), ], [ - $ownShelf->getUrl() => 'Edit' + $ownShelf->getUrl() => 'Edit', ]); $this->visit($otherShelf->getUrl()) @@ -316,9 +320,9 @@ class RolesTest extends BrowserKitTest { $otherShelf = Bookshelf::first(); $this->checkAccessPermission('bookshelf-update-all', [ - $otherShelf->getUrl('/edit') + $otherShelf->getUrl('/edit'), ], [ - $otherShelf->getUrl() => 'Edit' + $otherShelf->getUrl() => 'Edit', ]); } @@ -331,9 +335,9 @@ class RolesTest extends BrowserKitTest $this->regenEntityPermissions($ownShelf); $this->checkAccessPermission('bookshelf-delete-own', [ - $ownShelf->getUrl('/delete') + $ownShelf->getUrl('/delete'), ], [ - $ownShelf->getUrl() => 'Delete' + $ownShelf->getUrl() => 'Delete', ]); $this->visit($otherShelf->getUrl()) @@ -351,9 +355,9 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['bookshelf-update-all']); $otherShelf = Bookshelf::first(); $this->checkAccessPermission('bookshelf-delete-all', [ - $otherShelf->getUrl('/delete') + $otherShelf->getUrl('/delete'), ], [ - $otherShelf->getUrl() => 'Delete' + $otherShelf->getUrl() => 'Delete', ]); $this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete')) @@ -365,9 +369,9 @@ class RolesTest extends BrowserKitTest public function test_books_create_all_permissions() { $this->checkAccessPermission('book-create-all', [ - '/create-book' + '/create-book', ], [ - '/books' => 'Create New Book' + '/books' => 'Create New Book', ]); $this->visit('/create-book') @@ -382,9 +386,9 @@ class RolesTest extends BrowserKitTest $otherBook = Book::take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-update-own', [ - $ownBook->getUrl() . '/edit' + $ownBook->getUrl() . '/edit', ], [ - $ownBook->getUrl() => 'Edit' + $ownBook->getUrl() => 'Edit', ]); $this->visit($otherBook->getUrl()) @@ -397,9 +401,9 @@ class RolesTest extends BrowserKitTest { $otherBook = Book::take(1)->get()->first(); $this->checkAccessPermission('book-update-all', [ - $otherBook->getUrl() . '/edit' + $otherBook->getUrl() . '/edit', ], [ - $otherBook->getUrl() => 'Edit' + $otherBook->getUrl() => 'Edit', ]); } @@ -409,9 +413,9 @@ class RolesTest extends BrowserKitTest $otherBook = Book::take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('book-delete-own', [ - $ownBook->getUrl() . '/delete' + $ownBook->getUrl() . '/delete', ], [ - $ownBook->getUrl() => 'Delete' + $ownBook->getUrl() => 'Delete', ]); $this->visit($otherBook->getUrl()) @@ -429,9 +433,9 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['book-update-all']); $otherBook = Book::take(1)->get()->first(); $this->checkAccessPermission('book-delete-all', [ - $otherBook->getUrl() . '/delete' + $otherBook->getUrl() . '/delete', ], [ - $otherBook->getUrl() => 'Delete' + $otherBook->getUrl() => 'Delete', ]); $this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete') @@ -445,9 +449,9 @@ class RolesTest extends BrowserKitTest $book = Book::take(1)->get()->first(); $ownBook = $this->createEntityChainBelongingToUser($this->user)['book']; $this->checkAccessPermission('chapter-create-own', [ - $ownBook->getUrl('/create-chapter') + $ownBook->getUrl('/create-chapter'), ], [ - $ownBook->getUrl() => 'New Chapter' + $ownBook->getUrl() => 'New Chapter', ]); $this->visit($ownBook->getUrl('/create-chapter')) @@ -466,9 +470,9 @@ class RolesTest extends BrowserKitTest { $book = Book::take(1)->get()->first(); $this->checkAccessPermission('chapter-create-all', [ - $book->getUrl('/create-chapter') + $book->getUrl('/create-chapter'), ], [ - $book->getUrl() => 'New Chapter' + $book->getUrl() => 'New Chapter', ]); $this->visit($book->getUrl('/create-chapter')) @@ -483,9 +487,9 @@ class RolesTest extends BrowserKitTest $otherChapter = Chapter::take(1)->get()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-update-own', [ - $ownChapter->getUrl() . '/edit' + $ownChapter->getUrl() . '/edit', ], [ - $ownChapter->getUrl() => 'Edit' + $ownChapter->getUrl() => 'Edit', ]); $this->visit($otherChapter->getUrl()) @@ -498,9 +502,9 @@ class RolesTest extends BrowserKitTest { $otherChapter = Chapter::take(1)->get()->first(); $this->checkAccessPermission('chapter-update-all', [ - $otherChapter->getUrl() . '/edit' + $otherChapter->getUrl() . '/edit', ], [ - $otherChapter->getUrl() => 'Edit' + $otherChapter->getUrl() => 'Edit', ]); } @@ -510,9 +514,9 @@ class RolesTest extends BrowserKitTest $otherChapter = Chapter::take(1)->get()->first(); $ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter']; $this->checkAccessPermission('chapter-delete-own', [ - $ownChapter->getUrl() . '/delete' + $ownChapter->getUrl() . '/delete', ], [ - $ownChapter->getUrl() => 'Delete' + $ownChapter->getUrl() => 'Delete', ]); $bookUrl = $ownChapter->book->getUrl(); @@ -531,9 +535,9 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['chapter-update-all']); $otherChapter = Chapter::take(1)->get()->first(); $this->checkAccessPermission('chapter-delete-all', [ - $otherChapter->getUrl() . '/delete' + $otherChapter->getUrl() . '/delete', ], [ - $otherChapter->getUrl() => 'Delete' + $otherChapter->getUrl() => 'Delete', ]); $bookUrl = $otherChapter->book->getUrl(); @@ -562,8 +566,8 @@ class RolesTest extends BrowserKitTest } $this->checkAccessPermission('page-create-own', [], [ - $ownBook->getUrl() => 'New Page', - $ownChapter->getUrl() => 'New Page' + $ownBook->getUrl() => 'New Page', + $ownChapter->getUrl() => 'New Page', ]); $this->giveUserPermissions($this->user, ['page-create-own']); @@ -606,8 +610,8 @@ class RolesTest extends BrowserKitTest } $this->checkAccessPermission('page-create-all', [], [ - $book->getUrl() => 'New Page', - $chapter->getUrl() => 'New Page' + $book->getUrl() => 'New Page', + $chapter->getUrl() => 'New Page', ]); $this->giveUserPermissions($this->user, ['page-create-all']); @@ -636,9 +640,9 @@ class RolesTest extends BrowserKitTest $otherPage = Page::take(1)->get()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-update-own', [ - $ownPage->getUrl() . '/edit' + $ownPage->getUrl() . '/edit', ], [ - $ownPage->getUrl() => 'Edit' + $ownPage->getUrl() => 'Edit', ]); $this->visit($otherPage->getUrl()) @@ -651,9 +655,9 @@ class RolesTest extends BrowserKitTest { $otherPage = Page::take(1)->get()->first(); $this->checkAccessPermission('page-update-all', [ - $otherPage->getUrl() . '/edit' + $otherPage->getUrl() . '/edit', ], [ - $otherPage->getUrl() => 'Edit' + $otherPage->getUrl() => 'Edit', ]); } @@ -663,9 +667,9 @@ class RolesTest extends BrowserKitTest $otherPage = Page::take(1)->get()->first(); $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->checkAccessPermission('page-delete-own', [ - $ownPage->getUrl() . '/delete' + $ownPage->getUrl() . '/delete', ], [ - $ownPage->getUrl() => 'Delete' + $ownPage->getUrl() => 'Delete', ]); $parent = $ownPage->chapter ?? $ownPage->book; @@ -684,9 +688,9 @@ class RolesTest extends BrowserKitTest $this->giveUserPermissions($this->user, ['page-update-all']); $otherPage = Page::take(1)->get()->first(); $this->checkAccessPermission('page-delete-all', [ - $otherPage->getUrl() . '/delete' + $otherPage->getUrl() . '/delete', ], [ - $otherPage->getUrl() => 'Delete' + $otherPage->getUrl() => 'Delete', ]); $parent = $otherPage->chapter ?? $otherPage->book; @@ -702,8 +706,8 @@ class RolesTest extends BrowserKitTest $adminRole = Role::getSystemRole('admin'); $publicRole = Role::getSystemRole('public'); $this->asAdmin()->visit('/settings/users/' . $user->id) - ->seeElement('[name="roles['.$adminRole->id.']"]') - ->seeElement('[name="roles['.$publicRole->id.']"]'); + ->seeElement('[name="roles[' . $adminRole->id . ']"]') + ->seeElement('[name="roles[' . $publicRole->id . ']"]'); } public function test_public_role_visible_in_role_listing() @@ -779,8 +783,8 @@ class RolesTest extends BrowserKitTest $this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [ 'display_name' => $viewerRole->display_name, - 'description' => $viewerRole->description, - 'permission' => [] + 'description' => $viewerRole->description, + 'permission' => [], ])->assertResponseStatus(302); $this->expectException(HttpException::class); @@ -805,7 +809,8 @@ class RolesTest extends BrowserKitTest ->dontSee('Sort the current book'); } - public function test_comment_create_permission () { + public function test_comment_create_permission() + { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->actingAs($this->user)->addComment($ownPage); @@ -818,8 +823,8 @@ class RolesTest extends BrowserKitTest $this->assertResponseStatus(200); } - - public function test_comment_update_own_permission () { + public function test_comment_update_own_permission() + { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); $commentId = $this->actingAs($this->user)->addComment($ownPage); @@ -835,7 +840,8 @@ class RolesTest extends BrowserKitTest $this->assertResponseStatus(200); } - public function test_comment_update_all_permission () { + public function test_comment_update_all_permission() + { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $commentId = $this->asAdmin()->addComment($ownPage); @@ -850,7 +856,8 @@ class RolesTest extends BrowserKitTest $this->assertResponseStatus(200); } - public function test_comment_delete_own_permission () { + public function test_comment_delete_own_permission() + { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $this->giveUserPermissions($this->user, ['comment-create-all']); $commentId = $this->actingAs($this->user)->addComment($ownPage); @@ -866,7 +873,8 @@ class RolesTest extends BrowserKitTest $this->assertResponseStatus(200); } - public function test_comment_delete_all_permission () { + public function test_comment_delete_all_permission() + { $ownPage = $this->createEntityChainBelongingToUser($this->user)['page']; $commentId = $this->asAdmin()->addComment($ownPage); @@ -881,33 +889,37 @@ class RolesTest extends BrowserKitTest $this->assertResponseStatus(200); } - private function addComment($page) { + private function addComment($page) + { $comment = factory(Comment::class)->make(); $url = "/comment/$page->id"; $request = [ 'text' => $comment->text, - 'html' => $comment->html + 'html' => $comment->html, ]; $this->postJson($url, $request); $comment = $page->comments()->first(); + return $comment === null ? null : $comment->id; } - private function updateComment($commentId) { + private function updateComment($commentId) + { $comment = factory(Comment::class)->make(); $url = "/comment/$commentId"; $request = [ 'text' => $comment->text, - 'html' => $comment->html + 'html' => $comment->html, ]; return $this->putJson($url, $request); } - private function deleteComment($commentId) { - $url = '/comment/' . $commentId; - return $this->json('DELETE', $url); - } + private function deleteComment($commentId) + { + $url = '/comment/' . $commentId; + return $this->json('DELETE', $url); + } } diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index 7dbf467bd..ae0c0ff95 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -1,4 +1,6 @@ -setSettings(['app-public' => 'false']); @@ -27,7 +28,7 @@ class PublicActionTest extends TestCase public function test_login_link_visible() { $this->setSettings(['app-public' => 'true']); - $this->get('/')->assertElementExists('a[href="'.url('/login').'"]'); + $this->get('/')->assertElementExists('a[href="' . url('/login') . '"]'); } public function test_register_link_visible_when_enabled() @@ -94,22 +95,22 @@ class PublicActionTest extends TestCase $chapter = Chapter::query()->first(); $resp = $this->get($chapter->getUrl()); $resp->assertSee('New Page'); - $resp->assertElementExists('a[href="'.$chapter->getUrl('/create-page').'"]'); + $resp->assertElementExists('a[href="' . $chapter->getUrl('/create-page') . '"]'); $resp = $this->get($chapter->getUrl('/create-page')); $resp->assertSee('Continue'); $resp->assertSee('Page Name'); - $resp->assertElementExists('form[action="'.$chapter->getUrl('/create-guest-page').'"]'); + $resp->assertElementExists('form[action="' . $chapter->getUrl('/create-guest-page') . '"]'); $resp = $this->post($chapter->getUrl('/create-guest-page'), ['name' => 'My guest page']); $resp->assertRedirect($chapter->book->getUrl('/page/my-guest-page/edit')); $user = User::getDefault(); $this->assertDatabaseHas('pages', [ - 'name' => 'My guest page', + 'name' => 'My guest page', 'chapter_id' => $chapter->id, 'created_by' => $user->id, - 'updated_by' => $user->id + 'updated_by' => $user->id, ]); } @@ -137,7 +138,7 @@ class PublicActionTest extends TestCase $resp = $this->get('/robots.txt'); $resp->assertSee("User-agent: *\nDisallow:"); - $resp->assertDontSee("Disallow: /"); + $resp->assertDontSee('Disallow: /'); } public function test_robots_effected_by_setting() @@ -148,7 +149,7 @@ class PublicActionTest extends TestCase $resp = $this->get('/robots.txt'); $resp->assertSee("User-agent: *\nDisallow:"); - $resp->assertDontSee("Disallow: /"); + $resp->assertDontSee('Disallow: /'); // Check config overrides app-public setting config()->set('app.allow_robots', false); @@ -184,4 +185,4 @@ class PublicActionTest extends TestCase $resp->assertRedirect($book->getUrl()); $this->followRedirects($resp)->assertSee($book->name); } -} \ No newline at end of file +} diff --git a/tests/RecycleBinTest.php b/tests/RecycleBinTest.php index 55a9571de..1c5445212 100644 --- a/tests/RecycleBinTest.php +++ b/tests/RecycleBinTest.php @@ -1,4 +1,6 @@ -id}", ]; - foreach($routes as $route) { + foreach ($routes as $route) { [$method, $url] = explode(':', $route); $resp = $this->call($method, $url); $this->assertPermissionError($resp); @@ -35,7 +37,7 @@ class RecycleBinTest extends TestCase $this->giveUserPermissions($editor, ['restrictions-manage-all']); - foreach($routes as $route) { + foreach ($routes as $route) { [$method, $url] = explode(':', $route); $resp = $this->call($method, $url); $this->assertPermissionError($resp); @@ -43,14 +45,13 @@ class RecycleBinTest extends TestCase $this->giveUserPermissions($editor, ['settings-manage']); - foreach($routes as $route) { + foreach ($routes as $route) { DB::beginTransaction(); [$method, $url] = explode(':', $route); $resp = $this->call($method, $url); $this->assertNotPermissionError($resp); DB::rollBack(); } - } public function test_recycle_bin_view() @@ -72,7 +73,7 @@ class RecycleBinTest extends TestCase public function test_recycle_bin_empty() { $page = Page::query()->first(); - $book = Book::query()->where('id' , '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); $editor = $this->getEditor(); $this->actingAs($editor)->delete($page->getUrl()); $this->actingAs($editor)->delete($book->getUrl()); @@ -89,7 +90,7 @@ class RecycleBinTest extends TestCase $itemCount = 2 + $book->pages->count() + $book->chapters->count(); $redirectReq = $this->get('/settings/recycle-bin'); - $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin'); + $redirectReq->assertNotificationContains('Deleted ' . $itemCount . ' total items from the recycle bin'); } public function test_entity_restore() @@ -110,7 +111,7 @@ class RecycleBinTest extends TestCase $itemCount = 1 + $book->pages->count() + $book->chapters->count(); $redirectReq = $this->get('/settings/recycle-bin'); - $redirectReq->assertNotificationContains('Restored '.$itemCount.' total items from the recycle bin'); + $redirectReq->assertNotificationContains('Restored ' . $itemCount . ' total items from the recycle bin'); } public function test_permanent_delete() @@ -129,13 +130,13 @@ class RecycleBinTest extends TestCase $itemCount = 1 + $book->pages->count() + $book->chapters->count(); $redirectReq = $this->get('/settings/recycle-bin'); - $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin'); + $redirectReq->assertNotificationContains('Deleted ' . $itemCount . ' total items from the recycle bin'); } public function test_permanent_delete_for_each_type() { /** @var Entity $entity */ - foreach ([new Bookshelf, new Book, new Chapter, new Page] as $entity) { + foreach ([new Bookshelf(), new Book(), new Chapter(), new Page()] as $entity) { $entity = $entity->newQuery()->first(); $this->asEditor()->delete($entity->getUrl()); $deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail(); @@ -154,24 +155,24 @@ class RecycleBinTest extends TestCase $deletion = $page->deletions()->firstOrFail(); $this->assertDatabaseHas('activities', [ - 'type' => 'page_delete', - 'entity_id' => $page->id, + 'type' => 'page_delete', + 'entity_id' => $page->id, 'entity_type' => $page->getMorphClass(), ]); $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}"); $this->assertDatabaseMissing('activities', [ - 'type' => 'page_delete', - 'entity_id' => $page->id, + 'type' => 'page_delete', + 'entity_id' => $page->id, 'entity_type' => $page->getMorphClass(), ]); $this->assertDatabaseHas('activities', [ - 'type' => 'page_delete', - 'entity_id' => null, + 'type' => 'page_delete', + 'entity_id' => null, 'entity_type' => null, - 'detail' => $page->name, + 'detail' => $page->name, ]); } @@ -233,8 +234,8 @@ class RecycleBinTest extends TestCase $chapterRestoreView->assertSeeText($chapter->name); $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore"); - $chapterRestore->assertRedirect("/settings/recycle-bin"); - $this->assertDatabaseMissing("deletions", ["id" => $chapterDeletion->id]); + $chapterRestore->assertRedirect('/settings/recycle-bin'); + $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]); $chapter->refresh(); $this->assertNotNull($chapter->deleted_at); @@ -247,4 +248,22 @@ class RecycleBinTest extends TestCase $chapter->refresh(); $this->assertNull($chapter->deleted_at); } -} \ No newline at end of file + + public function test_restore_page_shows_link_to_parent_restore_if_parent_also_deleted() + { + /** @var Book $book */ + $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail(); + $chapter = $book->chapters->first(); + /** @var Page $page */ + $page = $chapter->pages->first(); + $this->asEditor()->delete($page->getUrl()); + $this->asEditor()->delete($book->getUrl()); + + $bookDeletion = $book->deletions()->first(); + $pageDeletion = $page->deletions()->first(); + + $pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore"); + $pageRestoreView->assertSee('The parent of this item has also been deleted.'); + $pageRestoreView->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent'); + } +} diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index db095ff70..888dac810 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -1,40 +1,40 @@ -get("/"); + $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { - $this->assertEquals("lax", $cookie->getSameSite()); + $this->assertEquals('lax', $cookie->getSameSite()); } } public function test_cookies_samesite_none_when_iframe_hosts_set() { - $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "http://example.com", function() { - $resp = $this->get("/"); + $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'http://example.com', function () { + $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { - $this->assertEquals("none", $cookie->getSameSite()); + $this->assertEquals('none', $cookie->getSameSite()); } }); } public function test_secure_cookies_controlled_by_app_url() { - $this->runWithEnv("APP_URL", "http://example.com", function() { - $resp = $this->get("/"); + $this->runWithEnv('APP_URL', 'http://example.com', function () { + $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { $this->assertFalse($cookie->isSecure()); } }); - $this->runWithEnv("APP_URL", "https://example.com", function() { - $resp = $this->get("/"); + $this->runWithEnv('APP_URL', 'https://example.com', function () { + $resp = $this->get('/'); foreach ($resp->headers->getCookies() as $cookie) { $this->assertTrue($cookie->isSecure()); } @@ -43,7 +43,7 @@ class SecurityHeaderTest extends TestCase public function test_iframe_csp_self_only_by_default() { - $resp = $this->get("/"); + $resp = $this->get('/'); $cspHeaders = collect($resp->headers->get('Content-Security-Policy')); $frameHeaders = $cspHeaders->filter(function ($val) { return Str::startsWith($val, 'frame-ancestors'); @@ -55,17 +55,15 @@ class SecurityHeaderTest extends TestCase public function test_iframe_csp_includes_extra_hosts_if_configured() { - $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "https://a.example.com https://b.example.com", function() { - $resp = $this->get("/"); + $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'https://a.example.com https://b.example.com', function () { + $resp = $this->get('/'); $cspHeaders = collect($resp->headers->get('Content-Security-Policy')); - $frameHeaders = $cspHeaders->filter(function($val) { + $frameHeaders = $cspHeaders->filter(function ($val) { return Str::startsWith($val, 'frame-ancestors'); }); $this->assertTrue($frameHeaders->count() === 1); $this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeaders->first()); }); - } - -} \ No newline at end of file +} diff --git a/tests/SharedTestHelpers.php b/tests/SharedTestHelpers.php index a98f01e94..df6c613df 100644 --- a/tests/SharedTestHelpers.php +++ b/tests/SharedTestHelpers.php @@ -1,5 +1,11 @@ -actingAs($this->getEditor()); } - /** * Get a editor user. */ @@ -67,6 +69,7 @@ trait SharedTestHelpers $editorRole = Role::getRole('editor'); $this->editor = $editorRole->users->first(); } + return $this->editor; } @@ -79,6 +82,7 @@ trait SharedTestHelpers if (!empty($attributes)) { $user->forceFill($attributes)->save(); } + return $user; } @@ -116,7 +120,7 @@ trait SharedTestHelpers } /** - * Create and return a new test chapter + * Create and return a new test chapter. */ public function newChapter(array $input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book): Chapter { @@ -124,13 +128,14 @@ trait SharedTestHelpers } /** - * Create and return a new test page + * Create and return a new test page. */ public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page { $book = Book::query()->first(); $pageRepo = app(PageRepo::class); $draftPage = $pageRepo->getNewDraftPage($book); + return $pageRepo->publishDraft($draftPage, $input); } @@ -158,7 +163,7 @@ trait SharedTestHelpers foreach ($roles as $role) { $permissions[] = [ 'role_id' => $role->id, - 'action' => strtolower($action) + 'action' => strtolower($action), ]; } } @@ -181,6 +186,19 @@ trait SharedTestHelpers $user->clearPermissionCache(); } + /** + * Completely remove the given permission name from the given user. + */ + protected function removePermissionFromUser(User $user, string $permission) + { + $permission = RolePermission::query()->where('name', '=', $permission)->first(); + /** @var Role $role */ + foreach ($user->roles as $role) { + $role->detachPermission($permission); + } + $user->clearPermissionCache(); + } + /** * Create a new basic role for testing purposes. */ @@ -189,6 +207,7 @@ trait SharedTestHelpers $permissionRepo = app(PermissionsRepo::class); $roleData = factory(Role::class)->make()->toArray(); $roleData['permissions'] = array_flip($permissions); + return $permissionRepo->saveNewRole($roleData); } @@ -253,7 +272,7 @@ trait SharedTestHelpers */ protected function assertPermissionError($response) { - PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response contains a permission error."); + PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response contains a permission error.'); } /** @@ -261,7 +280,7 @@ trait SharedTestHelpers */ protected function assertNotPermissionError($response) { - PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response does not contain a permission error."); + PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response does not contain a permission error.'); } /** @@ -270,8 +289,17 @@ trait SharedTestHelpers private function isPermissionError($response): bool { return $response->status() === 302 - && $response->headers->get('Location') === url('/') - && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0; + && ( + ( + $response->headers->get('Location') === url('/') + && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0 + ) + || + ( + $response instanceof JsonResponse && + $response->json(['error' => 'You do not have permission to perform the requested action.']) + ) + ); } /** @@ -291,5 +319,4 @@ trait SharedTestHelpers return $testHandler; } - -} \ No newline at end of file +} diff --git a/tests/StatusTest.php b/tests/StatusTest.php index b4c35cf91..09882759c 100644 --- a/tests/StatusTest.php +++ b/tests/StatusTest.php @@ -1,8 +1,8 @@ get("/status"); + $resp = $this->get('/status'); $resp->assertStatus(200); $resp->assertJson([ 'database' => true, - 'cache' => true, - 'session' => true, + 'cache' => true, + 'session' => true, ]); } @@ -23,7 +23,7 @@ class StatusTest extends TestCase { DB::shouldReceive('table')->andThrow(new Exception()); - $resp = $this->get("/status"); + $resp = $this->get('/status'); $resp->assertStatus(500); $resp->assertJson([ 'database' => false, @@ -36,7 +36,7 @@ class StatusTest extends TestCase Cache::swap($mockStore); $mockStore->shouldReceive('get')->andReturn('cat'); - $resp = $this->get("/status"); + $resp = $this->get('/status'); $resp->assertStatus(500); $resp->assertJson([ 'cache' => false, @@ -50,10 +50,10 @@ class StatusTest extends TestCase Session::swap($mockSession); $mockSession->shouldReceive('get')->andReturn('cat'); - $resp = $this->get("/status"); + $resp = $this->get('/status'); $resp->assertStatus(500); $resp->assertJson([ 'session' => false, ]); } -} \ No newline at end of file +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2c901981a..080515173 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,4 +1,6 @@ -assertTrue(session()->has($key), "Session does not contain a [{$key}] entry"); + return $this; } /** * Override of the get method so we can get visibility of custom TestResponse methods. - * @param string $uri - * @param array $headers + * + * @param string $uri + * @param array $headers + * * @return TestResponse */ public function get($uri, array $headers = []) @@ -41,7 +49,8 @@ abstract class TestCase extends BaseTestCase /** * Create the test response instance from the given response. * - * @param \Illuminate\Http\Response $response + * @param \Illuminate\Http\Response $response + * * @return TestResponse */ protected function createTestResponse($response) @@ -64,4 +73,4 @@ abstract class TestCase extends BaseTestCase $this->assertDatabaseHas('activities', $detailsToCheck); } -} \ No newline at end of file +} diff --git a/tests/TestEmailTest.php b/tests/TestEmailTest.php index 76ff322ff..0a2091fe3 100644 --- a/tests/TestEmailTest.php +++ b/tests/TestEmailTest.php @@ -1,4 +1,6 @@ -asAdmin()->get('/settings/maintenance'); @@ -57,6 +58,4 @@ class TestEmailTest extends TestCase $sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email'); Notification::assertSentTo($user, TestEmail::class); } - - -} \ No newline at end of file +} diff --git a/tests/TestResponse.php b/tests/TestResponse.php index bf7ee0f69..79f173c9b 100644 --- a/tests/TestResponse.php +++ b/tests/TestResponse.php @@ -1,16 +1,17 @@ -crawlerInstance)) { $this->crawlerInstance = new Crawler($this->getContent()); } + return $this->crawlerInstance; } + /** + * Get the HTML of the first element at the given selector. + */ + public function getElementHtml(string $selector): string + { + return $this->crawler()->filter($selector)->first()->outerHtml(); + } + /** * Assert the response contains the specified element. + * * @return $this */ public function assertElementExists(string $selector) @@ -33,16 +44,18 @@ class TestResponse extends BaseTestResponse { $elements = $this->crawler()->filter($selector); PHPUnit::assertTrue( $elements->count() > 0, - 'Unable to find element matching the selector: '.PHP_EOL.PHP_EOL. - "[{$selector}]".PHP_EOL.PHP_EOL. - 'within'.PHP_EOL.PHP_EOL. + 'Unable to find element matching the selector: ' . PHP_EOL . PHP_EOL . + "[{$selector}]" . PHP_EOL . PHP_EOL . + 'within' . PHP_EOL . PHP_EOL . "[{$this->getContent()}]." ); + return $this; } /** * Assert the response does not contain the specified element. + * * @return $this */ public function assertElementNotExists(string $selector) @@ -50,11 +63,12 @@ class TestResponse extends BaseTestResponse { $elements = $this->crawler()->filter($selector); PHPUnit::assertTrue( $elements->count() === 0, - 'Found elements matching the selector: '.PHP_EOL.PHP_EOL. - "[{$selector}]".PHP_EOL.PHP_EOL. - 'within'.PHP_EOL.PHP_EOL. + 'Found elements matching the selector: ' . PHP_EOL . PHP_EOL . + "[{$selector}]" . PHP_EOL . PHP_EOL . + 'within' . PHP_EOL . PHP_EOL . "[{$this->getContent()}]." ); + return $this; } @@ -62,6 +76,7 @@ class TestResponse extends BaseTestResponse { * Assert the response includes a specific element containing the given text. * If an nth match is provided, only that will be checked otherwise all matching * elements will be checked for the given text. + * * @return $this */ public function assertElementContains(string $selector, string $text, ?int $nthMatch = null) @@ -84,12 +99,12 @@ class TestResponse extends BaseTestResponse { PHPUnit::assertTrue( $matched, - 'Unable to find element of selector: '.PHP_EOL.PHP_EOL. - ($nthMatch ? ("at position {$nthMatch}".PHP_EOL.PHP_EOL) : '') . - "[{$selector}]".PHP_EOL.PHP_EOL. - 'containing text'.PHP_EOL.PHP_EOL. - "[{$text}]".PHP_EOL.PHP_EOL. - 'within'.PHP_EOL.PHP_EOL. + 'Unable to find element of selector: ' . PHP_EOL . PHP_EOL . + ($nthMatch ? ("at position {$nthMatch}" . PHP_EOL . PHP_EOL) : '') . + "[{$selector}]" . PHP_EOL . PHP_EOL . + 'containing text' . PHP_EOL . PHP_EOL . + "[{$text}]" . PHP_EOL . PHP_EOL . + 'within' . PHP_EOL . PHP_EOL . "[{$this->getContent()}]." ); @@ -100,6 +115,7 @@ class TestResponse extends BaseTestResponse { * Assert the response does not include a specific element containing the given text. * If an nth match is provided, only that will be checked otherwise all matching * elements will be checked for the given text. + * * @return $this */ public function assertElementNotContains(string $selector, string $text, ?int $nthMatch = null) @@ -122,12 +138,12 @@ class TestResponse extends BaseTestResponse { PHPUnit::assertTrue( !$matched, - 'Found element of selector: '.PHP_EOL.PHP_EOL. - ($nthMatch ? ("at position {$nthMatch}".PHP_EOL.PHP_EOL) : '') . - "[{$selector}]".PHP_EOL.PHP_EOL. - 'containing text'.PHP_EOL.PHP_EOL. - "[{$text}]".PHP_EOL.PHP_EOL. - 'within'.PHP_EOL.PHP_EOL. + 'Found element of selector: ' . PHP_EOL . PHP_EOL . + ($nthMatch ? ("at position {$nthMatch}" . PHP_EOL . PHP_EOL) : '') . + "[{$selector}]" . PHP_EOL . PHP_EOL . + 'containing text' . PHP_EOL . PHP_EOL . + "[{$text}]" . PHP_EOL . PHP_EOL . + 'within' . PHP_EOL . PHP_EOL . "[{$this->getContent()}]." ); @@ -136,6 +152,7 @@ class TestResponse extends BaseTestResponse { /** * Assert there's a notification within the view containing the given text. + * * @return $this */ public function assertNotificationContains(string $text) @@ -145,14 +162,15 @@ class TestResponse extends BaseTestResponse { /** * Get the escaped text pattern for the constraint. + * * @return string */ protected function getEscapedPattern(string $text) { $rawPattern = preg_quote($text, '/'); $escapedPattern = preg_quote(e($text), '/'); + return $rawPattern == $escapedPattern ? $rawPattern : "({$rawPattern}|{$escapedPattern})"; } - } diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index be3fc4ebd..bab85be7a 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -1,4 +1,6 @@ -assertInstanceOf(ConfigurableEnvironmentInterface::class, $environment); $callbackCalled = true; + return $environment; }; Theme::listen(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $callback); @@ -158,8 +161,8 @@ class ThemeTest extends TestCase public function test_add_social_driver() { Theme::addSocialDriver('catnet', [ - 'client_id' => 'abc123', - 'client_secret' => 'def456' + 'client_id' => 'abc123', + 'client_secret' => 'def456', ], 'SocialiteProviders\Discord\DiscordExtendSocialite@handleTesting'); $this->assertEquals('catnet', config('services.catnet.name')); @@ -173,9 +176,9 @@ class ThemeTest extends TestCase public function test_add_social_driver_uses_name_in_config_if_given() { Theme::addSocialDriver('catnet', [ - 'client_id' => 'abc123', + 'client_id' => 'abc123', 'client_secret' => 'def456', - 'name' => 'Super Cat Name', + 'name' => 'Super Cat Name', ], 'SocialiteProviders\Discord\DiscordExtendSocialite@handleTesting'); $this->assertEquals('Super Cat Name', config('services.catnet.name')); @@ -183,15 +186,14 @@ class ThemeTest extends TestCase $loginResp->assertSee('Super Cat Name'); } - public function test_add_social_driver_allows_a_configure_for_redirect_callback_to_be_passed() { Theme::addSocialDriver( 'discord', [ - 'client_id' => 'abc123', + 'client_id' => 'abc123', 'client_secret' => 'def456', - 'name' => 'Super Cat Name', + 'name' => 'Super Cat Name', ], 'SocialiteProviders\Discord\DiscordExtendSocialite@handle', function ($driver) { @@ -204,11 +206,10 @@ class ThemeTest extends TestCase $this->assertStringContainsString('donkey=donut', $redirect); } - protected function usingThemeFolder(callable $callback) { // Create a folder and configure a theme - $themeFolderName = 'testing_theme_' . rtrim(base64_encode(time()), "="); + $themeFolderName = 'testing_theme_' . rtrim(base64_encode(time()), '='); config()->set('view.theme', $themeFolderName); $themeFolderPath = theme_path(''); File::makeDirectory($themeFolderPath); @@ -218,5 +219,4 @@ class ThemeTest extends TestCase // Cleanup the custom theme folder we created File::deleteDirectory($themeFolderPath); } - -} \ No newline at end of file +} diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index 0833ffbd8..207fb7f59 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -1,4 +1,6 @@ -runWithEnv('STORAGE_TYPE', 'local_secure', function() { + $this->runWithEnv('STORAGE_TYPE', 'local_secure', function () { $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3'); $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure'); }); @@ -23,7 +22,7 @@ class ConfigTest extends TestCase public function test_filesystem_attachments_falls_back_to_storage_type_var() { - $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() { + $this->runWithEnv('STORAGE_TYPE', 'local_secure', function () { $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3'); $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure'); }); @@ -46,11 +45,11 @@ class ConfigTest extends TestCase ]); $temp = tempnam(sys_get_temp_dir(), 'bs-test'); - $original = ini_set( 'error_log', $temp); + $original = ini_set('error_log', $temp); Log::channel('errorlog_plain_webserver')->info('Aww, look, a cute puppy'); - ini_set( 'error_log', $original); + ini_set('error_log', $original); $output = file_get_contents($temp); $this->assertStringContainsString('Aww, look, a cute puppy', $output); @@ -77,17 +76,23 @@ class ConfigTest extends TestCase ); } + public function test_dompdf_remote_fetching_controlled_by_allow_untrusted_server_fetching_false() + { + $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'false', 'dompdf.defines.enable_remote', false); + $this->checkEnvConfigResult('ALLOW_UNTRUSTED_SERVER_FETCHING', 'true', 'dompdf.defines.enable_remote', true); + } + /** * Set an environment variable of the given name and value * then check the given config key to see if it matches the given result. * Providing a null $envVal clears the variable. + * * @param mixed $expectedResult */ protected function checkEnvConfigResult(string $envName, ?string $envVal, string $configKey, $expectedResult) { - $this->runWithEnv($envName, $envVal, function() use ($configKey, $expectedResult) { + $this->runWithEnv($envName, $envVal, function () use ($configKey, $expectedResult) { $this->assertEquals($expectedResult, config($configKey)); }); } - -} \ No newline at end of file +} diff --git a/tests/Unit/UrlTest.php b/tests/Unit/UrlTest.php index b9f485da1..fff5414f2 100644 --- a/tests/Unit/UrlTest.php +++ b/tests/Unit/UrlTest.php @@ -1,22 +1,22 @@ -runWithEnv('APP_URL', 'http://example.com/bookstack', function() { + $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function () { $this->assertEquals('http://example.com/bookstack/books', url('/books')); }); } public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different() { - $this->runWithEnv('APP_URL', 'https://example.com/', function() { + $this->runWithEnv('APP_URL', 'https://example.com/', function () { $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css'); }); } - -} \ No newline at end of file +} diff --git a/tests/Uploads/AttachmentTest.php b/tests/Uploads/AttachmentTest.php index 55a5aa84f..2248bc2c5 100644 --- a/tests/Uploads/AttachmentTest.php +++ b/tests/Uploads/AttachmentTest.php @@ -1,9 +1,11 @@ -getTestFile($name); + return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); } /** - * Create a new attachment + * Create a new attachment. */ protected function createAttachment(Page $page): Attachment { $this->post('attachments/link', [ - 'attachment_link_url' => 'https://example.com', - 'attachment_link_name' => 'Example Attachment Link', + 'attachment_link_url' => 'https://example.com', + 'attachment_link_name' => 'Example Attachment Link', 'attachment_link_uploaded_to' => $page->id, ]); @@ -61,10 +64,10 @@ class AttachmentTest extends TestCase $fileName = 'upload_test_file.txt'; $expectedResp = [ - 'name' => $fileName, + 'name' => $fileName, 'uploaded_to'=> $page->id, - 'extension' => 'txt', - 'order' => 1, + 'extension' => 'txt', + 'order' => 1, 'created_by' => $admin->id, 'updated_by' => $admin->id, ]; @@ -86,7 +89,6 @@ class AttachmentTest extends TestCase $page = Page::query()->first(); $fileName = 'upload_test_file.txt'; - $upload = $this->asAdmin()->uploadFile($fileName, $page->id); $upload->assertStatus(200); @@ -122,20 +124,20 @@ class AttachmentTest extends TestCase $this->asAdmin(); $linkReq = $this->call('POST', 'attachments/link', [ - 'attachment_link_url' => 'https://example.com', - 'attachment_link_name' => 'Example Attachment Link', + 'attachment_link_url' => 'https://example.com', + 'attachment_link_name' => 'Example Attachment Link', 'attachment_link_uploaded_to' => $page->id, ]); $expectedData = [ - 'path' => 'https://example.com', - 'name' => 'Example Attachment Link', + 'path' => 'https://example.com', + 'name' => 'Example Attachment Link', 'uploaded_to' => $page->id, - 'created_by' => $admin->id, - 'updated_by' => $admin->id, - 'external' => true, - 'order' => 1, - 'extension' => '' + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + 'external' => true, + 'order' => 1, + 'extension' => '', ]; $linkReq->assertStatus(200); @@ -160,14 +162,14 @@ class AttachmentTest extends TestCase $attachment = $this->createAttachment($page); $update = $this->call('PUT', 'attachments/' . $attachment->id, [ 'attachment_edit_name' => 'My new attachment name', - 'attachment_edit_url' => 'https://test.example.com' + 'attachment_edit_url' => 'https://test.example.com', ]); $expectedData = [ - 'id' => $attachment->id, - 'path' => 'https://test.example.com', - 'name' => 'My new attachment name', - 'uploaded_to' => $page->id + 'id' => $attachment->id, + 'path' => 'https://test.example.com', + 'name' => 'My new attachment name', + 'uploaded_to' => $page->id, ]; $update->assertStatus(200); @@ -191,7 +193,7 @@ class AttachmentTest extends TestCase $this->delete($attachment->getUrl()); $this->assertDatabaseMissing('attachments', [ - 'name' => $fileName + 'name' => $fileName, ]); $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected'); @@ -210,14 +212,14 @@ class AttachmentTest extends TestCase $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist'); $this->assertDatabaseHas('attachments', [ - 'name' => $fileName + 'name' => $fileName, ]); app(PageRepo::class)->destroy($page); app(TrashCan::class)->empty(); $this->assertDatabaseMissing('attachments', [ - 'name' => $fileName + 'name' => $fileName, ]); $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected'); @@ -229,7 +231,6 @@ class AttachmentTest extends TestCase $admin = $this->getAdmin(); $viewer = $this->getViewer(); $page = Page::query()->first(); /** @var Page $page */ - $this->actingAs($admin); $fileName = 'permission_test.txt'; $this->uploadFile($fileName, $page->id); @@ -244,7 +245,7 @@ class AttachmentTest extends TestCase $this->actingAs($viewer); $attachmentGet = $this->get($attachment->getUrl()); $attachmentGet->assertStatus(404); - $attachmentGet->assertSee("Attachment not found"); + $attachmentGet->assertSee('Attachment not found'); $this->deleteUploads(); } @@ -259,15 +260,15 @@ class AttachmentTest extends TestCase ' javascript:alert("bunny")', 'JavaScript:alert("bunny")', "\t\n\t\nJavaScript:alert(\"bunny\")", - "data:text/html;", - "Data:text/html;", - "Data:text/html;", + 'data:text/html;', + 'Data:text/html;', + 'Data:text/html;', ]; foreach ($badLinks as $badLink) { $linkReq = $this->post('attachments/link', [ - 'attachment_link_url' => $badLink, - 'attachment_link_name' => 'Example Attachment Link', + 'attachment_link_url' => $badLink, + 'attachment_link_name' => 'Example Attachment Link', 'attachment_link_uploaded_to' => $page->id, ]); $linkReq->assertStatus(422); @@ -280,7 +281,7 @@ class AttachmentTest extends TestCase foreach ($badLinks as $badLink) { $linkReq = $this->put('attachments/' . $attachment->id, [ - 'attachment_edit_url' => $badLink, + 'attachment_edit_url' => $badLink, 'attachment_edit_name' => 'Example Attachment Link', ]); $linkReq->assertStatus(422); @@ -303,7 +304,7 @@ class AttachmentTest extends TestCase $attachmentGet = $this->get($attachment->getUrl(true)); // http-foundation/Response does some 'fixing' of responses to add charsets to text responses. $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8'); - $attachmentGet->assertHeader('Content-Disposition', "inline; filename=\"upload_test_file.txt\""); + $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"'); $this->deleteUploads(); } diff --git a/tests/Uploads/AvatarTest.php b/tests/Uploads/AvatarTest.php index efaa016dd..cf568d07c 100644 --- a/tests/Uploads/AvatarTest.php +++ b/tests/Uploads/AvatarTest.php @@ -1,24 +1,25 @@ -asAdmin()->post('/settings/users/create', [ - 'name' => $user->name, - 'email' => $user->email, - 'password' => 'testing', + 'name' => $user->name, + 'email' => $user->email, + 'password' => 'testing', 'password-confirm' => 'testing', ]); + return User::where('email', '=', $user->email)->first(); } @@ -42,26 +43,25 @@ class AvatarTest extends TestCase 'services.disable_services' => false, ]); $user = factory(User::class)->make(); - $this->assertImageFetchFrom('https://www.gravatar.com/avatar/'.md5(strtolower($user->email)).'?s=500&d=identicon'); + $this->assertImageFetchFrom('https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon'); $user = $this->createUserRequest($user); $this->assertDatabaseHas('images', [ - 'type' => 'user', - 'created_by' => $user->id + 'type' => 'user', + 'created_by' => $user->id, ]); $this->deleteUserImage($user); } - public function test_custom_url_used_if_set() { config()->set([ 'services.disable_services' => false, - 'services.avatar_url' => 'https://example.com/${email}/${hash}/${size}', + 'services.avatar_url' => 'https://example.com/${email}/${hash}/${size}', ]); $user = factory(User::class)->make(); - $url = 'https://example.com/'. urlencode(strtolower($user->email)) .'/'. md5(strtolower($user->email)).'/500'; + $url = 'https://example.com/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500'; $this->assertImageFetchFrom($url); $user = $this->createUserRequest($user); @@ -97,5 +97,4 @@ class AvatarTest extends TestCase $this->createUserRequest($user); $this->assertTrue($logger->hasError('Failed to save user avatar image')); } - } diff --git a/tests/Uploads/DrawioTest.php b/tests/Uploads/DrawioTest.php index d134135aa..422de472a 100644 --- a/tests/Uploads/DrawioTest.php +++ b/tests/Uploads/DrawioTest.php @@ -1,4 +1,6 @@ -getJson("/images/drawio/base64/{$image->id}"); $imageGet->assertJson([ - 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' + 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=', ]); } @@ -33,23 +35,23 @@ class DrawioTest extends TestCase $upload = $this->postJson('images/drawio', [ 'uploaded_to' => $page->id, - 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' + 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=', ]); $upload->assertStatus(200); $upload->assertJson([ - 'type' => 'drawio', + 'type' => 'drawio', 'uploaded_to' => $page->id, - 'created_by' => $editor->id, - 'updated_by' => $editor->id, + 'created_by' => $editor->id, + 'updated_by' => $editor->id, ]); $image = Image::where('type', '=', 'drawio')->first(); - $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: '. public_path($image->path)); + $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: ' . public_path($image->path)); $testImageData = file_get_contents($this->getTestImageFilePath()); $uploadedImageData = file_get_contents(public_path($image->path)); - $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected"); + $this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected'); } public function test_drawio_url_can_be_configured() @@ -75,5 +77,4 @@ class DrawioTest extends TestCase $resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp->assertDontSee('drawio-url'); } - -} \ No newline at end of file +} diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 95332565e..69b6dc90e 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -1,15 +1,16 @@ -uploadGalleryImage($page); $relPath = $imgDetails['path']; - $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath)); + $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath)); $this->deleteImage($relPath); $this->assertDatabaseHas('images', [ - 'url' => $this->baseUrl . $relPath, - 'type' => 'gallery', + 'url' => $this->baseUrl . $relPath, + 'type' => 'gallery', 'uploaded_to' => $page->id, - 'path' => $relPath, - 'created_by' => $admin->id, - 'updated_by' => $admin->id, - 'name' => $imgDetails['name'], + 'path' => $relPath, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + 'name' => $imgDetails['name'], ]); } @@ -47,7 +48,7 @@ class ImageTest extends TestCase $imgDetails = $this->uploadGalleryImage($page, 'compressed.png'); $relPath = $imgDetails['path']; - $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath)); + $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath)); $displayImage = $imgDetails['response']->thumbs->display; $displayImageRelPath = implode('/', array_slice(explode('/', $displayImage), 3)); @@ -77,7 +78,7 @@ class ImageTest extends TestCase $this->assertDatabaseHas('images', [ 'type' => 'gallery', - 'name' => $newName + 'name' => $newName, ]); } @@ -115,7 +116,7 @@ class ImageTest extends TestCase $imgDetails = $this->uploadGalleryImage($page); $image = Image::query()->first(); - $page->html = ''; + $page->html = ''; $page->save(); $usage = $this->get('/images/edit/' . $image->id . '?delete=true'); @@ -144,7 +145,7 @@ class ImageTest extends TestCase $this->assertDatabaseMissing('images', [ 'type' => 'gallery', - 'name' => $fileName + 'name' => $fileName, ]); } @@ -194,11 +195,11 @@ class ImageTest extends TestCase { $this->asEditor(); $badNames = [ - "bad-char-#-image.png", - "bad-char-?-image.png", - "?#.png", - "?.png", - "#.png", + 'bad-char-#-image.png', + 'bad-char-?-image.png', + '?#.png', + '?.png', + '#.png', ]; foreach ($badNames as $name) { $galleryFile = $this->getTestImage($name); @@ -228,12 +229,12 @@ class ImageTest extends TestCase $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); $page = Page::query()->first(); - $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png'); + $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png'); $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $upload->assertStatus(200); - $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); + $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath); if (file_exists($expectedPath)) { unlink($expectedPath); @@ -246,7 +247,7 @@ class ImageTest extends TestCase $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); $page = Page::query()->first(); - $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png'); + $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png'); $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $imageUrl = json_decode($upload->getContent(), true)['url']; @@ -268,12 +269,12 @@ class ImageTest extends TestCase config()->set('filesystems.images', 'local_secure'); $this->asAdmin(); $galleryFile = $this->getTestImage('my-system-test-upload.png'); - $expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png'); + $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-upload.png'); $upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []); $upload->assertRedirect('/settings'); - $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); + $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath); if (file_exists($expectedPath)) { unlink($expectedPath); @@ -291,12 +292,12 @@ class ImageTest extends TestCase $this->uploadImage($imageName, $page->id); $image = Image::first(); - $delete = $this->delete( '/images/' . $image->id); + $delete = $this->delete('/images/' . $image->id); $delete->assertStatus(200); $this->assertDatabaseMissing('images', [ - 'url' => $this->baseUrl . $relPath, - 'type' => 'gallery' + 'url' => $this->baseUrl . $relPath, + 'type' => 'gallery', ]); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected'); @@ -319,7 +320,7 @@ class ImageTest extends TestCase $folder = public_path(dirname($relPath)); $imageCount = count(glob($folder . '/*')); - $delete = $this->delete( '/images/' . $image->id); + $delete = $this->delete('/images/' . $image->id); $delete->assertStatus(200); $newCount = count(glob($folder . '/*')); @@ -346,9 +347,9 @@ class ImageTest extends TestCase $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); $this->assertDatabaseHas('images', [ - 'type' => 'user', + 'type' => 'user', 'uploaded_to' => $editor->id, - 'created_by' => $admin->id, + 'created_by' => $admin->id, ]); } @@ -361,7 +362,7 @@ class ImageTest extends TestCase $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get(); - $this->assertTrue($profileImages->count() === 1, "Found profile images does not match upload count"); + $this->assertTrue($profileImages->count() === 1, 'Found profile images does not match upload count'); $imagePath = public_path($profileImages->first()->path); $this->assertTrue(file_exists($imagePath)); @@ -370,12 +371,12 @@ class ImageTest extends TestCase $userDelete->assertStatus(302); $this->assertDatabaseMissing('images', [ - 'type' => 'user', - 'created_by' => $editor->id + 'type' => 'user', + 'created_by' => $editor->id, ]); $this->assertDatabaseMissing('images', [ - 'type' => 'user', - 'uploaded_to' => $editor->id + 'type' => 'user', + 'uploaded_to' => $editor->id, ]); $this->assertFalse(file_exists($imagePath)); @@ -397,9 +398,9 @@ class ImageTest extends TestCase $pageRepo = app(PageRepo::class); $pageRepo->update($page, [ - 'name' => $page->name, - 'html' => $page->html . "url}\">", - 'summary' => '' + 'name' => $page->name, + 'html' => $page->html . "url}\">", + 'summary' => '', ]); // Ensure no images are reported as deletable @@ -409,9 +410,9 @@ class ImageTest extends TestCase // Save a revision of our page without the image; $pageRepo->update($page, [ - 'name' => $page->name, - 'html' => "

        Hello

        ", - 'summary' => '' + 'name' => $page->name, + 'html' => '

        Hello

        ', + 'summary' => '', ]); // Ensure revision images are picked up okay @@ -435,5 +436,4 @@ class ImageTest extends TestCase $this->deleteImage($relPath); } - } diff --git a/tests/Uploads/UsesImages.php b/tests/Uploads/UsesImages.php index 24c253802..789c967c6 100644 --- a/tests/Uploads/UsesImages.php +++ b/tests/Uploads/UsesImages.php @@ -1,4 +1,6 @@ -getTestImage($name, $testDataFileName); + return $this->withHeader('Content-Type', $contentType) ->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); } @@ -75,7 +82,9 @@ trait UsesImages * Upload a new gallery image. * Returns the image name. * Can provide a page to relate the image to. + * * @param Page|null $page + * * @return array */ protected function uploadGalleryImage(Page $page = null, ?string $testDataFileName = null) @@ -90,10 +99,11 @@ trait UsesImages $upload = $this->uploadImage($imageName, $page->id, 'image/png', $testDataFileName); $upload->assertStatus(200); + return [ - 'name' => $imageName, - 'path' => $relPath, - 'page' => $page, + 'name' => $imageName, + 'path' => $relPath, + 'page' => $page, 'response' => json_decode($upload->getContent()), ]; } @@ -108,5 +118,4 @@ trait UsesImages unlink($path); } } - -} \ No newline at end of file +} diff --git a/tests/User/UserApiTokenTest.php b/tests/User/UserApiTokenTest.php index df686dd77..d3404b72e 100644 --- a/tests/User/UserApiTokenTest.php +++ b/tests/User/UserApiTokenTest.php @@ -1,4 +1,6 @@ - 'My test API token', + 'name' => 'My test API token', 'expires_at' => '2050-04-01', ]; @@ -51,8 +52,8 @@ class UserApiTokenTest extends TestCase $token = ApiToken::query()->latest()->first(); $resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id)); $this->assertDatabaseHas('api_tokens', [ - 'user_id' => $editor->id, - 'name' => $this->testTokenData['name'], + 'user_id' => $editor->id, + 'name' => $this->testTokenData['name'], 'expires_at' => $this->testTokenData['expires_at'], ]); @@ -81,7 +82,7 @@ class UserApiTokenTest extends TestCase $under = Carbon::now()->addYears(99); $this->assertTrue( ($token->expires_at < $over && $token->expires_at > $under), - "Token expiry set at 100 years in future" + 'Token expiry set at 100 years in future' ); } @@ -117,7 +118,7 @@ class UserApiTokenTest extends TestCase $this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData); $token = ApiToken::query()->latest()->first(); $updateData = [ - 'name' => 'My updated token', + 'name' => 'My updated token', 'expires_at' => '2011-01-01', ]; @@ -136,7 +137,7 @@ class UserApiTokenTest extends TestCase $token = ApiToken::query()->latest()->first(); $resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), [ - 'name' => 'My updated token', + 'name' => 'My updated token', 'expires_at' => '', ]); $token->refresh(); @@ -145,7 +146,7 @@ class UserApiTokenTest extends TestCase $under = Carbon::now()->addYears(99); $this->assertTrue( ($token->expires_at < $over && $token->expires_at > $under), - "Token expiry set at 100 years in future" + 'Token expiry set at 100 years in future' ); } @@ -160,7 +161,7 @@ class UserApiTokenTest extends TestCase $resp = $this->get($tokenUrl . '/delete'); $resp->assertSeeText('Delete Token'); $resp->assertSeeText($token->name); - $resp->assertElementExists('form[action="'.$tokenUrl.'"]'); + $resp->assertElementExists('form[action="' . $tokenUrl . '"]'); $resp = $this->delete($tokenUrl); $resp->assertRedirect($editor->getEditUrl('#api_tokens')); @@ -185,5 +186,4 @@ class UserApiTokenTest extends TestCase $resp->assertRedirect($viewer->getEditUrl('#api_tokens')); $this->assertDatabaseMissing('api_tokens', ['id' => $token->id]); } - -} \ No newline at end of file +} diff --git a/tests/User/UserManagementTest.php b/tests/User/UserManagementTest.php index d99d61401..4fd7bacc7 100644 --- a/tests/User/UserManagementTest.php +++ b/tests/User/UserManagementTest.php @@ -1,4 +1,6 @@ -getEditor(); $resp = $this->asAdmin()->delete("settings/users/{$editor->id}"); - $resp->assertRedirect("/settings/users"); + $resp->assertRedirect('/settings/users'); $resp = $this->followRedirects($resp); - $resp->assertSee("User successfully removed"); + $resp->assertSee('User successfully removed'); $this->assertActivityExists(ActivityType::USER_DELETE); $this->assertDatabaseMissing('users', ['id' => $editor->id]); @@ -25,20 +26,20 @@ class UserManagementTest extends TestCase { $editor = $this->getEditor(); $resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete"); - $resp->assertSee("Migrate Ownership"); - $resp->assertSee("new_owner_id"); + $resp->assertSee('Migrate Ownership'); + $resp->assertSee('new_owner_id'); } public function test_delete_with_new_owner_id_changes_ownership() { $page = Page::query()->first(); $owner = $page->ownedBy; - $newOwner = User::query()->where('id', '!=' , $owner->id)->first(); + $newOwner = User::query()->where('id', '!=', $owner->id)->first(); $this->asAdmin()->delete("settings/users/{$owner->id}", ['new_owner_id' => $newOwner->id]); $this->assertDatabaseHas('pages', [ - 'id' => $page->id, + 'id' => $page->id, 'owned_by' => $newOwner->id, ]); } -} \ No newline at end of file +} diff --git a/tests/User/UserPreferencesTest.php b/tests/User/UserPreferencesTest.php index 49c49188b..1d5d3e729 100644 --- a/tests/User/UserPreferencesTest.php +++ b/tests/User/UserPreferencesTest.php @@ -1,28 +1,29 @@ -getEditor(); $this->actingAs($editor); - $updateRequest = $this->patch('/settings/users/' . $editor->id.'/change-sort/books', [ - 'sort' => 'created_at', - 'order' => 'desc' + $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/books', [ + 'sort' => 'created_at', + 'order' => 'desc', ]); $updateRequest->assertStatus(302); $this->assertDatabaseHas('settings', [ 'setting_key' => 'user:' . $editor->id . ':books_sort', - 'value' => 'created_at' + 'value' => 'created_at', ]); $this->assertDatabaseHas('settings', [ 'setting_key' => 'user:' . $editor->id . ':books_sort_order', - 'value' => 'desc' + 'value' => 'desc', ]); $this->assertEquals('created_at', setting()->getForCurrentUser('books_sort')); $this->assertEquals('desc', setting()->getForCurrentUser('books_sort_order')); @@ -33,9 +34,9 @@ class UserPreferencesTest extends TestCase $editor = $this->getEditor(); $this->actingAs($editor); - $updateRequest = $this->patch('/settings/users/' . $editor->id.'/change-sort/bookshelves', [ - 'sort' => 'cat', - 'order' => 'dog' + $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/bookshelves', [ + 'sort' => 'cat', + 'order' => 'dog', ]); $updateRequest->assertStatus(302); @@ -48,9 +49,9 @@ class UserPreferencesTest extends TestCase $editor = $this->getEditor(); $this->actingAs($editor); - $updateRequest = $this->patch('/settings/users/' . $editor->id.'/change-sort/dogs', [ - 'sort' => 'name', - 'order' => 'asc' + $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/dogs', [ + 'sort' => 'name', + 'order' => 'asc', ]); $updateRequest->assertStatus(500); @@ -63,16 +64,16 @@ class UserPreferencesTest extends TestCase $editor = $this->getEditor(); $this->actingAs($editor); - $updateRequest = $this->patch('/settings/users/' . $editor->id.'/update-expansion-preference/home-details', ['expand' => 'true']); + $updateRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/home-details', ['expand' => 'true']); $updateRequest->assertStatus(204); $this->assertDatabaseHas('settings', [ 'setting_key' => 'user:' . $editor->id . ':section_expansion#home-details', - 'value' => 'true' + 'value' => 'true', ]); $this->assertEquals(true, setting()->getForCurrentUser('section_expansion#home-details')); - $invalidKeyRequest = $this->patch('/settings/users/' . $editor->id.'/update-expansion-preference/my-home-details', ['expand' => 'true']); + $invalidKeyRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/my-home-details', ['expand' => 'true']); $invalidKeyRequest->assertStatus(500); } @@ -105,4 +106,4 @@ class UserPreferencesTest extends TestCase $home = $this->get('/login'); $home->assertElementExists('.dark-mode'); } -} \ No newline at end of file +} diff --git a/tests/User/UserProfileTest.php b/tests/User/UserProfileTest.php index c87996669..859a036e0 100644 --- a/tests/User/UserProfileTest.php +++ b/tests/User/UserProfileTest.php @@ -1,4 +1,6 @@ -pageNotHasElement('.content-wrap .entity-list-item') ->see('List View'); } - }