Merge branch 'master' into release

This commit is contained in:
Dan Brown 2021-10-25 15:58:59 +01:00
commit f88687e977
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
174 changed files with 4933 additions and 652 deletions

View File

@ -232,6 +232,8 @@ SAML2_ONELOGIN_OVERRIDES=null
SAML2_DUMP_USER_DETAILS=false
SAML2_AUTOLOAD_METADATA=false
SAML2_IDP_AUTHNCONTEXT=true
SAML2_SP_x509=null
SAML2_SP_x509_KEY=null
# SAML group sync configuration
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
@ -239,6 +241,18 @@ SAML2_USER_TO_GROUPS=false
SAML2_GROUP_ATTRIBUTE=group
SAML2_REMOVE_FROM_GROUPS=false
# OpenID Connect authentication configuration
OIDC_NAME=SSO
OIDC_DISPLAY_NAME_CLAIMS=name
OIDC_CLIENT_ID=null
OIDC_CLIENT_SECRET=null
OIDC_ISSUER=null
OIDC_ISSUER_DISCOVER=false
OIDC_PUBLIC_KEY=null
OIDC_AUTH_ENDPOINT=null
OIDC_TOKEN_ENDPOINT=null
OIDC_DUMP_USER_DETAILS=false
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
DISABLE_EXTERNAL_SERVICES=false

View File

@ -1,17 +0,0 @@
---
name: New API Endpoint or Feature
about: Request a new endpoint or API feature be added
labels: ":nut_and_bolt: API Request"
---
#### API Endpoint or Feature
Clearly describe what you'd like to have added to the API.
#### Use-Case
Explain the use-case that you're working-on that requires the above request.
#### Additional Context
If required, add any other context about the feature request here.

26
.github/ISSUE_TEMPLATE/api_request.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: New API Endpoint or API Ability
description: Request a new endpoint or API feature be added
title: "[API Request]: "
labels: [":nut_and_bolt: API Request"]
body:
- type: textarea
id: feature
attributes:
label: API Endpoint or Feature
description: Clearly describe what you'd like to have added to the API.
validations:
required: true
- type: textarea
id: usecase
attributes:
label: Use-Case
description: Explain the use-case that you're working-on that requires the above request.
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context about the feature request here.
validations:
required: false

View File

@ -1,29 +0,0 @@
---
name: Bug Report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Your Configuration (please complete the following information):**
- Exact BookStack Version (Found in settings):
- PHP Version:
- Hosting Method (Nginx/Apache/Docker):
**Additional context**
Add any other context about the problem here.

62
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Bug Report
description: Create a report to help us improve or fix things
title: "[Bug Report]: "
labels: [":bug: Bug"]
body:
- type: textarea
id: description
attributes:
label: Describe the Bug
description: Provide a clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Detail the steps that would replicate this issue
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behaviour
description: Provide clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: context
attributes:
label: Screenshots or Additional Context
description: Provide any additional context and screenshots here to help us solve this issue
validations:
required: false
- type: input
id: bsversion
attributes:
label: Exact BookStack Version
description: This can be found in the settings view of BookStack. Please provide an exact version.
placeholder: (eg. v21.08.5)
validations:
required: true
- type: input
id: phpversion
attributes:
label: PHP Version
description: Keep in mind your command-line PHP version may differ to that of your webserver. Provide that relevant to the issue.
placeholder: (eg. 7.4)
validations:
required: false
- type: textarea
id: hosting
attributes:
label: Hosting Environment
description: Describe your hosting environment as much as possible including any proxies used (If applicable).
placeholder: (eg. Ubuntu 20.04 VPS, installed using official installation script)
validations:
required: true

View File

@ -1,14 +0,0 @@
---
name: Feature Request
about: Suggest an idea for this project
---
**Describe the feature you'd like**
A clear description of the feature you'd like implemented in BookStack.
**Describe the benefits this feature would bring to BookStack users**
Explain the measurable benefits this feature would achieve.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,26 @@
name: Feature Request
description: Request a new language to be added to CrowdIn for you to translate
title: "[Feature Request]: "
labels: [":hammer: Feature Request"]
body:
- type: textarea
id: description
attributes:
label: Describe the feature you'd like
description: Provide a clear description of the feature you'd like implemented in BookStack
validations:
required: true
- type: textarea
id: benefits
attributes:
label: Describe the benefits this feature would bring to BookStack users
description: Explain the measurable benefits this feature would achieve for existing BookStack users
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false

View File

@ -1,13 +0,0 @@
---
name: Language Request
about: Request a new language to be added to Crowdin for you to translate
---
### Language To Add
_Specify here the language you want to add._
----
_This issue template is to request a new language be added to our [Crowdin translation management project](https://crowdin.com/project/bookstack). Please don't use this template to request a new language that you are not prepared to provide translations for._

View File

@ -0,0 +1,32 @@
name: Language Request
description: Request a new language to be added to CrowdIn for you to translate
title: "[Language Request]: "
labels: [":earth_africa: Translations"]
assignees:
- ssddanbrown
body:
- type: markdown
attributes:
value: |
Thanks for offering to help start a new translation for BookStack!
- type: input
id: language
attributes:
label: Language to Add
description: What language (and region if applicable) are you offering to help add to BookStack?
validations:
required: true
- type: checkboxes
id: confirm
attributes:
label: Confirmation of Intent
description: |
This issue template is to request a new language be added to our [Crowdin translation management project](https://crowdin.com/project/bookstack).
Please don't use this template to request a new language that you are not prepared to provide translations for.
options:
- label: I confirm I'm offering to help translate for this new language via CrowdIn.
required: true
- type: markdown
attributes:
value: |
*__Note: New languages are added at specific points of the development process so it may be a small while before the requested language is added for translation.__*

View File

@ -0,0 +1,63 @@
name: Support Request
description: Request support for a specific problem you have not been able to solve yourself
title: "[Support Request]: "
labels: [":dog2: Support"]
body:
- type: checkboxes
id: useddocs
attributes:
label: Attempted Debugging
description: |
I have read the [BookStack debugging](https://www.bookstackapp.com/docs/admin/debugging/) page and seeked resolution or more
detail for the issue.
options:
- label: I have read the debugging page
required: true
- type: checkboxes
id: searchissue
attributes:
label: Searched GitHub Issues
description: |
I have searched for the issue and potential resolutions within the [project's GitHub issue list](https://github.com/BookStackApp/BookStack/issues)
options:
- label: I have searched GitHub for the issue.
required: true
- type: textarea
id: scenario
attributes:
label: Describe the Scenario
description: Detail the problem that you're having or what you need support with.
validations:
required: true
- type: input
id: bsversion
attributes:
label: Exact BookStack Version
description: This can be found in the settings view of BookStack. Please provide an exact version.
placeholder: (eg. v21.08.5)
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log Content
description: If the issue has produced an error, provide any [BookStack or server log](https://www.bookstackapp.com/docs/admin/debugging/) content below.
placeholder: Be sure to remove any confidential details in your logs
validations:
required: false
- type: input
id: phpversion
attributes:
label: PHP Version
description: Keep in mind your command-line PHP version may differ to that of your webserver. Provide that most relevant to the issue.
placeholder: (eg. 7.4)
validations:
required: false
- type: textarea
id: hosting
attributes:
label: Hosting Environment
description: Describe your hosting environment as much as possible including any proxies used (If applicable).
placeholder: (eg. Ubuntu 20.04 VPS, installed using official installation script)
validations:
required: true

View File

@ -192,3 +192,7 @@ Atalonica :: Catalan
慕容潭谈 (591442386) :: Chinese Simplified
Radim Pesek (ramess18) :: Czech
anastasiia.motylko :: Ukrainian
Indrek Haav (IndrekHaav) :: Estonian
na3shkw :: Japanese
Giancarlo Di Massa (digitall-it) :: Italian
M Nafis Al Mukhdi (mnafisalmukhdi1) :: Indonesian

View File

@ -6,7 +6,7 @@ use BookStack\Auth\Role;
use BookStack\Auth\User;
use Illuminate\Support\Collection;
class ExternalAuthService
class GroupSyncService
{
/**
* Check a role against an array of group names to see if it matches.
@ -60,13 +60,13 @@ class ExternalAuthService
/**
* Sync the groups to the user roles for the current user.
*/
public function syncWithGroups(User $user, array $userGroups): void
public function syncUserWithFoundGroups(User $user, array $userGroups, bool $detachExisting): void
{
// Get the ids for the roles from the names
$groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups);
// Sync groups
if ($this->config['remove_from_groups']) {
if ($detachExisting) {
$user->roles()->sync($groupsAsRoles);
$user->attachDefaultRole();
} else {

View File

@ -10,7 +10,7 @@ namespace BookStack\Auth\Access\Guards;
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
* version of SessionGuard.
*/
class Saml2SessionGuard extends ExternalBaseSessionGuard
class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
{
/**
* Validate a user's credentials.

View File

@ -13,9 +13,10 @@ use Illuminate\Support\Facades\Log;
* Class LdapService
* Handles any app-specific LDAP tasks.
*/
class LdapService extends ExternalAuthService
class LdapService
{
protected $ldap;
protected $groupSyncService;
protected $ldapConnection;
protected $userAvatars;
protected $config;
@ -24,20 +25,19 @@ class LdapService extends ExternalAuthService
/**
* LdapService constructor.
*/
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
public function __construct(Ldap $ldap, UserAvatars $userAvatars, GroupSyncService $groupSyncService)
{
$this->ldap = $ldap;
$this->userAvatars = $userAvatars;
$this->groupSyncService = $groupSyncService;
$this->config = config('services.ldap');
$this->enabled = config('auth.method') === 'ldap';
}
/**
* Check if groups should be synced.
*
* @return bool
*/
public function shouldSyncGroups()
public function shouldSyncGroups(): bool
{
return $this->enabled && $this->config['user_to_groups'] !== false;
}
@ -285,9 +285,8 @@ class LdapService extends ExternalAuthService
}
$userGroups = $this->groupFilter($user);
$userGroups = $this->getGroupsRecursive($userGroups, []);
return $userGroups;
return $this->getGroupsRecursive($userGroups, []);
}
/**
@ -374,7 +373,7 @@ class LdapService extends ExternalAuthService
public function syncGroups(User $user, string $username)
{
$userLdapGroups = $this->getUserGroups($username);
$this->syncWithGroups($user, $userLdapGroups);
$this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']);
}
/**

View File

@ -47,7 +47,7 @@ class LoginService
// Authenticate on all session guards if a likely admin
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
$guards = ['standard', 'ldap', 'saml2'];
$guards = ['standard', 'ldap', 'saml2', 'oidc'];
foreach ($guards as $guard) {
auth($guard)->login($user);
}

View File

@ -0,0 +1,53 @@
<?php
namespace BookStack\Auth\Access\Oidc;
use InvalidArgumentException;
use League\OAuth2\Client\Token\AccessToken;
class OidcAccessToken extends AccessToken
{
/**
* Constructs an access token.
*
* @param array $options An array of options returned by the service provider
* in the access token request. The `access_token` option is required.
*
* @throws InvalidArgumentException if `access_token` is not provided in `$options`.
*/
public function __construct(array $options = [])
{
parent::__construct($options);
$this->validate($options);
}
/**
* Validate this access token response for OIDC.
* As per https://openid.net/specs/openid-connect-basic-1_0.html#TokenOK.
*/
private function validate(array $options): void
{
// access_token: REQUIRED. Access Token for the UserInfo Endpoint.
// Performed on the extended class
// token_type: REQUIRED. OAuth 2.0 Token Type value. The value MUST be Bearer, as specified in OAuth 2.0
// Bearer Token Usage [RFC6750], for Clients using this subset.
// Note that the token_type value is case-insensitive.
if (strtolower(($options['token_type'] ?? '')) !== 'bearer') {
throw new InvalidArgumentException('The response token type MUST be "Bearer"');
}
// id_token: REQUIRED. ID Token.
if (empty($options['id_token'])) {
throw new InvalidArgumentException('An "id_token" property must be provided');
}
}
/**
* Get the id token value from this access token response.
*/
public function getIdToken(): string
{
return $this->getValues()['id_token'];
}
}

View File

@ -0,0 +1,238 @@
<?php
namespace BookStack\Auth\Access\Oidc;
class OidcIdToken
{
/**
* @var array
*/
protected $header;
/**
* @var array
*/
protected $payload;
/**
* @var string
*/
protected $signature;
/**
* @var array[]|string[]
*/
protected $keys;
/**
* @var string
*/
protected $issuer;
/**
* @var array
*/
protected $tokenParts = [];
public function __construct(string $token, string $issuer, array $keys)
{
$this->keys = $keys;
$this->issuer = $issuer;
$this->parse($token);
}
/**
* Parse the token content into its components.
*/
protected function parse(string $token): void
{
$this->tokenParts = explode('.', $token);
$this->header = $this->parseEncodedTokenPart($this->tokenParts[0]);
$this->payload = $this->parseEncodedTokenPart($this->tokenParts[1] ?? '');
$this->signature = $this->base64UrlDecode($this->tokenParts[2] ?? '') ?: '';
}
/**
* Parse a Base64-JSON encoded token part.
* Returns the data as a key-value array or empty array upon error.
*/
protected function parseEncodedTokenPart(string $part): array
{
$json = $this->base64UrlDecode($part) ?: '{}';
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Base64URL decode. Needs some character conversions to be compatible
* with PHP's default base64 handling.
*/
protected function base64UrlDecode(string $encoded): string
{
return base64_decode(strtr($encoded, '-_', '+/'));
}
/**
* Validate all possible parts of the id token.
*
* @throws OidcInvalidTokenException
*/
public function validate(string $clientId): bool
{
$this->validateTokenStructure();
$this->validateTokenSignature();
$this->validateTokenClaims($clientId);
return true;
}
/**
* Fetch a specific claim from this token.
* Returns null if it is null or does not exist.
*
* @return mixed|null
*/
public function getClaim(string $claim)
{
return $this->payload[$claim] ?? null;
}
/**
* Get all returned claims within the token.
*/
public function getAllClaims(): array
{
return $this->payload;
}
/**
* Validate the structure of the given token and ensure we have the required pieces.
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
*
* @throws OidcInvalidTokenException
*/
protected function validateTokenStructure(): void
{
foreach (['header', 'payload'] as $prop) {
if (empty($this->$prop) || !is_array($this->$prop)) {
throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token");
}
}
if (empty($this->signature) || !is_string($this->signature)) {
throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token');
}
}
/**
* Validate the signature of the given token and ensure it validates against the provided key.
*
* @throws OidcInvalidTokenException
*/
protected function validateTokenSignature(): void
{
if ($this->header['alg'] !== 'RS256') {
throw new OidcInvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}");
}
$parsedKeys = array_map(function ($key) {
try {
return new OidcJwtSigningKey($key);
} catch (OidcInvalidKeyException $e) {
throw new OidcInvalidTokenException('Failed to read signing key with error: ' . $e->getMessage());
}
}, $this->keys);
$parsedKeys = array_filter($parsedKeys);
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
/** @var OidcJwtSigningKey $parsedKey */
foreach ($parsedKeys as $parsedKey) {
if ($parsedKey->verify($contentToSign, $this->signature)) {
return;
}
}
throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys');
}
/**
* Validate the claims of the token.
* As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.
*
* @throws OidcInvalidTokenException
*/
protected function validateTokenClaims(string $clientId): void
{
// 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
// MUST exactly match the value of the iss (issuer) Claim.
if (empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']) {
throw new OidcInvalidTokenException('Missing or non-matching token issuer value');
}
// 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
// at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected
// if the ID Token does not list the Client as a valid audience, or if it contains additional
// audiences not trusted by the Client.
if (empty($this->payload['aud'])) {
throw new OidcInvalidTokenException('Missing token audience value');
}
$aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud'];
if (count($aud) !== 1) {
throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1');
}
if ($aud[0] !== $clientId) {
throw new OidcInvalidTokenException('Token audience value did not match the expected client_id');
}
// 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
// NOTE: Addressed by enforcing a count of 1 above.
// 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id
// is the Claim Value.
if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) {
throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id');
}
// 5. The current time MUST be before the time represented by the exp Claim
// (possibly allowing for some small leeway to account for clock skew).
if (empty($this->payload['exp'])) {
throw new OidcInvalidTokenException('Missing token expiration time value');
}
$skewSeconds = 120;
$now = time();
if ($now >= (intval($this->payload['exp']) + $skewSeconds)) {
throw new OidcInvalidTokenException('Token has expired');
}
// 6. The iat Claim can be used to reject tokens that were issued too far away from the current time,
// limiting the amount of time that nonces need to be stored to prevent attacks.
// The acceptable range is Client specific.
if (empty($this->payload['iat'])) {
throw new OidcInvalidTokenException('Missing token issued at time value');
}
$dayAgo = time() - 86400;
$iat = intval($this->payload['iat']);
if ($iat > ($now + $skewSeconds) || $iat < $dayAgo) {
throw new OidcInvalidTokenException('Token issue at time is not recent or is invalid');
}
// 7. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate.
// The meaning and processing of acr Claim Values is out of scope for this document.
// NOTE: Not used for our case here. acr is not requested.
// 8. When a max_age request is made, the Client SHOULD check the auth_time Claim value and request
// re-authentication if it determines too much time has elapsed since the last End-User authentication.
// NOTE: Not used for our case here. A max_age request is not made.
// Custom: Ensure the "sub" (Subject) Claim exists and has a value.
if (empty($this->payload['sub'])) {
throw new OidcInvalidTokenException('Missing token subject value');
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace BookStack\Auth\Access\Oidc;
class OidcInvalidKeyException extends \Exception
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace BookStack\Auth\Access\Oidc;
use Exception;
class OidcInvalidTokenException extends Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace BookStack\Auth\Access\Oidc;
class OidcIssuerDiscoveryException extends \Exception
{
}

View File

@ -0,0 +1,109 @@
<?php
namespace BookStack\Auth\Access\Oidc;
use phpseclib3\Crypt\Common\PublicKey;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
use phpseclib3\Math\BigInteger;
class OidcJwtSigningKey
{
/**
* @var PublicKey
*/
protected $key;
/**
* Can be created either from a JWK parameter array or local file path to load a certificate from.
* Examples:
* 'file:///var/www/cert.pem'
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'].
*
* @param array|string $jwkOrKeyPath
*
* @throws OidcInvalidKeyException
*/
public function __construct($jwkOrKeyPath)
{
if (is_array($jwkOrKeyPath)) {
$this->loadFromJwkArray($jwkOrKeyPath);
} elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
$this->loadFromPath($jwkOrKeyPath);
} else {
throw new OidcInvalidKeyException('Unexpected type of key value provided');
}
}
/**
* @throws OidcInvalidKeyException
*/
protected function loadFromPath(string $path)
{
try {
$this->key = PublicKeyLoader::load(
file_get_contents($path)
)->withPadding(RSA::SIGNATURE_PKCS1);
} catch (\Exception $exception) {
throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
}
if (!($this->key instanceof RSA)) {
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
}
}
/**
* @throws OidcInvalidKeyException
*/
protected function loadFromJwkArray(array $jwk)
{
if ($jwk['alg'] !== 'RS256') {
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
}
if (empty($jwk['use'])) {
throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
}
if ($jwk['use'] !== 'sig') {
throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
}
if (empty($jwk['e'])) {
throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
}
if (empty($jwk['n'])) {
throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
}
$n = strtr($jwk['n'] ?? '', '-_', '+/');
try {
/** @var RSA $key */
$this->key = PublicKeyLoader::load([
'e' => new BigInteger(base64_decode($jwk['e']), 256),
'n' => new BigInteger(base64_decode($n), 256),
])->withPadding(RSA::SIGNATURE_PKCS1);
} catch (\Exception $exception) {
throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
}
}
/**
* Use this key to sign the given content and return the signature.
*/
public function verify(string $content, string $signature): bool
{
return $this->key->verify($content, $signature);
}
/**
* Convert the key to a PEM encoded key string.
*/
public function toPem(): string
{
return $this->key->toString('PKCS8');
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace BookStack\Auth\Access\Oidc;
use League\OAuth2\Client\Grant\AbstractGrant;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericResourceOwner;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;
/**
* Extended OAuth2Provider for using with OIDC.
* Credit to the https://github.com/steverhoades/oauth2-openid-connect-client
* project for the idea of extending a League\OAuth2 client for this use-case.
*/
class OidcOAuthProvider extends AbstractProvider
{
use BearerAuthorizationTrait;
/**
* @var string
*/
protected $authorizationEndpoint;
/**
* @var string
*/
protected $tokenEndpoint;
/**
* Returns the base URL for authorizing a client.
*/
public function getBaseAuthorizationUrl(): string
{
return $this->authorizationEndpoint;
}
/**
* Returns the base URL for requesting an access token.
*/
public function getBaseAccessTokenUrl(array $params): string
{
return $this->tokenEndpoint;
}
/**
* Returns the URL for requesting the resource owner's details.
*/
public function getResourceOwnerDetailsUrl(AccessToken $token): string
{
return '';
}
/**
* Returns the default scopes used by this provider.
*
* This should only be the scopes that are required to request the details
* of the resource owner, rather than all the available scopes.
*/
protected function getDefaultScopes(): array
{
return ['openid', 'profile', 'email'];
}
/**
* Returns the string that should be used to separate scopes when building
* the URL for requesting an access token.
*/
protected function getScopeSeparator(): string
{
return ' ';
}
/**
* Checks a provider response for errors.
*
* @param ResponseInterface $response
* @param array|string $data Parsed response data
*
* @throws IdentityProviderException
*
* @return void
*/
protected function checkResponse(ResponseInterface $response, $data)
{
if ($response->getStatusCode() >= 400 || isset($data['error'])) {
throw new IdentityProviderException(
$data['error'] ?? $response->getReasonPhrase(),
$response->getStatusCode(),
(string) $response->getBody()
);
}
}
/**
* Generates a resource owner object from a successful resource owner
* details request.
*
* @param array $response
* @param AccessToken $token
*
* @return ResourceOwnerInterface
*/
protected function createResourceOwner(array $response, AccessToken $token)
{
return new GenericResourceOwner($response, '');
}
/**
* Creates an access token from a response.
*
* The grant that was used to fetch the response can be used to provide
* additional context.
*
* @param array $response
* @param AbstractGrant $grant
*
* @return OidcAccessToken
*/
protected function createAccessToken(array $response, AbstractGrant $grant)
{
return new OidcAccessToken($response);
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace BookStack\Auth\Access\Oidc;
use GuzzleHttp\Psr7\Request;
use Illuminate\Contracts\Cache\Repository;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
/**
* OpenIdConnectProviderSettings
* Acts as a DTO for settings used within the oidc request and token handling.
* Performs auto-discovery upon request.
*/
class OidcProviderSettings
{
/**
* @var string
*/
public $issuer;
/**
* @var string
*/
public $clientId;
/**
* @var string
*/
public $clientSecret;
/**
* @var string
*/
public $redirectUri;
/**
* @var string
*/
public $authorizationEndpoint;
/**
* @var string
*/
public $tokenEndpoint;
/**
* @var string[]|array[]
*/
public $keys = [];
public function __construct(array $settings)
{
$this->applySettingsFromArray($settings);
$this->validateInitial();
}
/**
* Apply an array of settings to populate setting properties within this class.
*/
protected function applySettingsFromArray(array $settingsArray)
{
foreach ($settingsArray as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
/**
* Validate any core, required properties have been set.
*
* @throws InvalidArgumentException
*/
protected function validateInitial()
{
$required = ['clientId', 'clientSecret', 'redirectUri', 'issuer'];
foreach ($required as $prop) {
if (empty($this->$prop)) {
throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
}
}
if (strpos($this->issuer, 'https://') !== 0) {
throw new InvalidArgumentException('Issuer value must start with https://');
}
}
/**
* Perform a full validation on these settings.
*
* @throws InvalidArgumentException
*/
public function validate(): void
{
$this->validateInitial();
$required = ['keys', 'tokenEndpoint', 'authorizationEndpoint'];
foreach ($required as $prop) {
if (empty($this->$prop)) {
throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
}
}
}
/**
* Discover and autoload settings from the configured issuer.
*
* @throws OidcIssuerDiscoveryException
*/
public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes)
{
try {
$cacheKey = 'oidc-discovery::' . $this->issuer;
$discoveredSettings = $cache->remember($cacheKey, $cacheMinutes * 60, function () use ($httpClient) {
return $this->loadSettingsFromIssuerDiscovery($httpClient);
});
$this->applySettingsFromArray($discoveredSettings);
} catch (ClientExceptionInterface $exception) {
throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}");
}
}
/**
* @throws OidcIssuerDiscoveryException
* @throws ClientExceptionInterface
*/
protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array
{
$issuerUrl = rtrim($this->issuer, '/') . '/.well-known/openid-configuration';
$request = new Request('GET', $issuerUrl);
$response = $httpClient->sendRequest($request);
$result = json_decode($response->getBody()->getContents(), true);
if (empty($result) || !is_array($result)) {
throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}");
}
if ($result['issuer'] !== $this->issuer) {
throw new OidcIssuerDiscoveryException('Unexpected issuer value found on discovery response');
}
$discoveredSettings = [];
if (!empty($result['authorization_endpoint'])) {
$discoveredSettings['authorizationEndpoint'] = $result['authorization_endpoint'];
}
if (!empty($result['token_endpoint'])) {
$discoveredSettings['tokenEndpoint'] = $result['token_endpoint'];
}
if (!empty($result['jwks_uri'])) {
$keys = $this->loadKeysFromUri($result['jwks_uri'], $httpClient);
$discoveredSettings['keys'] = $this->filterKeys($keys);
}
return $discoveredSettings;
}
/**
* Filter the given JWK keys down to just those we support.
*/
protected function filterKeys(array $keys): array
{
return array_filter($keys, function (array $key) {
return $key['kty'] === 'RSA' && $key['use'] === 'sig' && $key['alg'] === 'RS256';
});
}
/**
* Return an array of jwks as PHP key=>value arrays.
*
* @throws ClientExceptionInterface
* @throws OidcIssuerDiscoveryException
*/
protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array
{
$request = new Request('GET', $uri);
$response = $httpClient->sendRequest($request);
$result = json_decode($response->getBody()->getContents(), true);
if (empty($result) || !is_array($result) || !isset($result['keys'])) {
throw new OidcIssuerDiscoveryException('Error reading keys from issuer jwks_uri');
}
return $result['keys'];
}
/**
* Get the settings needed by an OAuth provider, as a key=>value array.
*/
public function arrayForProvider(): array
{
$settingKeys = ['clientId', 'clientSecret', 'redirectUri', 'authorizationEndpoint', 'tokenEndpoint'];
$settings = [];
foreach ($settingKeys as $setting) {
$settings[$setting] = $this->$setting;
}
return $settings;
}
}

View File

@ -0,0 +1,221 @@
<?php
namespace BookStack\Auth\Access\Oidc;
use function auth;
use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\OpenIdConnectException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use function config;
use Exception;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface as HttpClient;
use function trans;
use function url;
/**
* Class OpenIdConnectService
* Handles any app-specific OIDC tasks.
*/
class OidcService
{
protected $registrationService;
protected $loginService;
protected $httpClient;
/**
* OpenIdService constructor.
*/
public function __construct(RegistrationService $registrationService, LoginService $loginService, HttpClient $httpClient)
{
$this->registrationService = $registrationService;
$this->loginService = $loginService;
$this->httpClient = $httpClient;
}
/**
* Initiate an authorization flow.
*
* @return array{url: string, state: string}
*/
public function login(): array
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
return [
'url' => $provider->getAuthorizationUrl(),
'state' => $provider->getState(),
];
}
/**
* Process the Authorization response from the authorization server and
* return the matching, or new if registration active, user matched to
* the authorization server.
* Returns null if not authenticated.
*
* @throws Exception
* @throws ClientExceptionInterface
*/
public function processAuthorizeResponse(?string $authorizationCode): ?User
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
// Try to exchange authorization code for access token
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $authorizationCode,
]);
return $this->processAccessTokenCallback($accessToken, $settings);
}
/**
* @throws OidcIssuerDiscoveryException
* @throws ClientExceptionInterface
*/
protected function getProviderSettings(): OidcProviderSettings
{
$config = $this->config();
$settings = new OidcProviderSettings([
'issuer' => $config['issuer'],
'clientId' => $config['client_id'],
'clientSecret' => $config['client_secret'],
'redirectUri' => url('/oidc/callback'),
'authorizationEndpoint' => $config['authorization_endpoint'],
'tokenEndpoint' => $config['token_endpoint'],
]);
// Use keys if configured
if (!empty($config['jwt_public_key'])) {
$settings->keys = [$config['jwt_public_key']];
}
// Run discovery
if ($config['discover'] ?? false) {
$settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
}
$settings->validate();
return $settings;
}
/**
* Load the underlying OpenID Connect Provider.
*/
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
{
return new OidcOAuthProvider($settings->arrayForProvider(), [
'httpClient' => $this->httpClient,
'optionProvider' => new HttpBasicAuthOptionProvider(),
]);
}
/**
* Calculate the display name.
*/
protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
{
$displayNameAttr = $this->config()['display_name_claims'];
$displayName = [];
foreach ($displayNameAttr as $dnAttr) {
$dnComponent = $token->getClaim($dnAttr) ?? '';
if ($dnComponent !== '') {
$displayName[] = $dnComponent;
}
}
if (count($displayName) == 0) {
$displayName[] = $defaultValue;
}
return implode(' ', $displayName);
}
/**
* Extract the details of a user from an ID token.
*
* @return array{name: string, email: string, external_id: string}
*/
protected function getUserDetails(OidcIdToken $token): array
{
$id = $token->getClaim('sub');
return [
'external_id' => $id,
'email' => $token->getClaim('email'),
'name' => $this->getUserDisplayName($token, $id),
];
}
/**
* Processes a received access token for a user. Login the user when
* they exist, optionally registering them automatically.
*
* @throws OpenIdConnectException
* @throws JsonDebugException
* @throws UserRegistrationException
* @throws StoppedAuthenticationException
*/
protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User
{
$idTokenText = $accessToken->getIdToken();
$idToken = new OidcIdToken(
$idTokenText,
$settings->issuer,
$settings->keys,
);
if ($this->config()['dump_user_details']) {
throw new JsonDebugException($idToken->getAllClaims());
}
try {
$idToken->validate($settings->clientId);
} catch (OidcInvalidTokenException $exception) {
throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}");
}
$userDetails = $this->getUserDetails($idToken);
$isLoggedIn = auth()->check();
if (empty($userDetails['email'])) {
throw new OpenIdConnectException(trans('errors.oidc_no_email_address'));
}
if ($isLoggedIn) {
throw new OpenIdConnectException(trans('errors.oidc_already_logged_in'), '/login');
}
$user = $this->registrationService->findOrRegister(
$userDetails['name'],
$userDetails['email'],
$userDetails['external_id']
);
if ($user === null) {
throw new OpenIdConnectException(trans('errors.oidc_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
}
$this->loginService->login($user, 'oidc');
return $user;
}
/**
* Get the OIDC config from the application.
*/
protected function config(): array
{
return config('oidc');
}
}

View File

@ -11,6 +11,7 @@ use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use Exception;
use Illuminate\Support\Str;
class RegistrationService
{
@ -50,6 +51,32 @@ class RegistrationService
return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');
}
/**
* Attempt to find a user in the system otherwise register them as a new
* user. For use with external auth systems since password is auto-generated.
*
* @throws UserRegistrationException
*/
public function findOrRegister(string $name, string $email, string $externalId): User
{
$user = User::query()
->where('external_auth_id', '=', $externalId)
->first();
if (is_null($user)) {
$userData = [
'name' => $name,
'email' => $email,
'password' => Str::random(32),
'external_auth_id' => $externalId,
];
$user = $this->registerUser($userData, null, false);
}
return $user;
}
/**
* The registrations flow for all users.
*

View File

@ -8,8 +8,8 @@ use BookStack\Exceptions\SamlException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use Exception;
use Illuminate\Support\Str;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Constants;
use OneLogin\Saml2\Error;
use OneLogin\Saml2\IdPMetadataParser;
use OneLogin\Saml2\ValidationError;
@ -18,20 +18,25 @@ use OneLogin\Saml2\ValidationError;
* Class Saml2Service
* Handles any app-specific SAML tasks.
*/
class Saml2Service extends ExternalAuthService
class Saml2Service
{
protected $config;
protected $registrationService;
protected $loginService;
protected $groupSyncService;
/**
* Saml2Service constructor.
*/
public function __construct(RegistrationService $registrationService, LoginService $loginService)
{
public function __construct(
RegistrationService $registrationService,
LoginService $loginService,
GroupSyncService $groupSyncService
) {
$this->config = config('saml2');
$this->registrationService = $registrationService;
$this->loginService = $loginService;
$this->groupSyncService = $groupSyncService;
}
/**
@ -55,13 +60,20 @@ class Saml2Service extends ExternalAuthService
*
* @throws Error
*/
public function logout(): array
public function logout(User $user): array
{
$toolKit = $this->getToolkit();
$returnRoute = url('/');
try {
$url = $toolKit->logout($returnRoute, [], null, null, true);
$url = $toolKit->logout(
$returnRoute,
[],
$user->email,
null,
true,
Constants::NAMEID_EMAIL_ADDRESS
);
$id = $toolKit->getLastRequestID();
} catch (Error $error) {
if ($error->getCode() !== Error::SAML_SINGLE_LOGOUT_NOT_SUPPORTED) {
@ -87,8 +99,11 @@ class Saml2Service extends ExternalAuthService
* @throws JsonDebugException
* @throws UserRegistrationException
*/
public function processAcsResponse(?string $requestId): ?User
public function processAcsResponse(string $requestId, string $samlResponse): ?User
{
// The SAML2 toolkit expects the response to be within the $_POST superglobal
// so we need to manually put it back there at this point.
$_POST['SAMLResponse'] = $samlResponse;
$toolkit = $this->getToolkit();
$toolkit->processResponse($requestId);
$errors = $toolkit->getErrors();
@ -117,8 +132,13 @@ class Saml2Service extends ExternalAuthService
public function processSlsResponse(?string $requestId): ?string
{
$toolkit = $this->getToolkit();
$redirect = $toolkit->processSLO(true, $requestId, false, null, true);
// The $retrieveParametersFromServer in the call below will mean the library will take the query
// parameters, used for the response signing, from the raw $_SERVER['QUERY_STRING']
// value so that the exact encoding format is matched when checking the signature.
// This is primarily due to ADFS encoding query params with lowercase percent encoding while
// PHP (And most other sensible providers) standardise on uppercase.
$redirect = $toolkit->processSLO(true, $requestId, true, null, true);
$errors = $toolkit->getErrors();
if (!empty($errors)) {
@ -258,6 +278,8 @@ class Saml2Service extends ExternalAuthService
/**
* Extract the details of a user from a SAML response.
*
* @return array{external_id: string, name: string, email: string, saml_id: string}
*/
protected function getUserDetails(string $samlID, $samlAttributes): array
{
@ -322,31 +344,6 @@ class Saml2Service extends ExternalAuthService
return $defaultValue;
}
/**
* Get the user from the database for the specified details.
*
* @throws UserRegistrationException
*/
protected function getOrRegisterUser(array $userDetails): ?User
{
$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),
'external_auth_id' => $userDetails['external_id'],
];
$user = $this->registrationService->registerUser($userData, null, false);
}
return $user;
}
/**
* Process the SAML response for a user. Login the user when
* they exist, optionally registering them automatically.
@ -377,14 +374,19 @@ class Saml2Service extends ExternalAuthService
throw new SamlException(trans('errors.saml_already_logged_in'), '/login');
}
$user = $this->getOrRegisterUser($userDetails);
$user = $this->registrationService->findOrRegister(
$userDetails['name'],
$userDetails['email'],
$userDetails['external_id']
);
if ($user === null) {
throw new SamlException(trans('errors.saml_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
}
if ($this->shouldSyncGroups()) {
$groups = $this->getUserGroups($samlAttributes);
$this->syncWithGroups($user, $groups);
$this->groupSyncService->syncUserWithFoundGroups($user, $groups, $this->config['remove_from_groups']);
}
$this->loginService->login($user, 'saml2');

View File

@ -61,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', 'lt', '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', 'et', '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',

View File

@ -11,7 +11,7 @@
return [
// Method of authentication to use
// Options: standard, ldap, saml2
// Options: standard, ldap, saml2, oidc
'method' => env('AUTH_METHOD', 'standard'),
// Authentication Defaults
@ -26,7 +26,7 @@ return [
// All authentication drivers have a user provider. This defines how the
// users are actually retrieved out of your database or other storage
// mechanisms used by this application to persist your user's data.
// Supported drivers: "session", "api-token", "ldap-session"
// Supported drivers: "session", "api-token", "ldap-session", "async-external-session"
'guards' => [
'standard' => [
'driver' => 'session',
@ -37,7 +37,11 @@ return [
'provider' => 'external',
],
'saml2' => [
'driver' => 'saml2-session',
'driver' => 'async-external-session',
'provider' => 'external',
],
'oidc' => [
'driver' => 'async-external-session',
'provider' => 'external',
],
'api' => [

35
app/Config/oidc.php Normal file
View File

@ -0,0 +1,35 @@
<?php
return [
// Display name, shown to users, for OpenId option
'name' => env('OIDC_NAME', 'SSO'),
// Dump user details after a login request for debugging purposes
'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),
// Attribute, within a OpenId token, to find the user's display name
'display_name_claims' => explode('|', env('OIDC_DISPLAY_NAME_CLAIMS', 'name')),
// OAuth2/OpenId client id, as configured in your Authorization server.
'client_id' => env('OIDC_CLIENT_ID', null),
// OAuth2/OpenId client secret, as configured in your Authorization server.
'client_secret' => env('OIDC_CLIENT_SECRET', null),
// The issuer of the identity token (id_token) this will be compared with
// what is returned in the token.
'issuer' => env('OIDC_ISSUER', null),
// Auto-discover the relevant endpoints and keys from the issuer.
// Fetched details are cached for 15 minutes.
'discover' => env('OIDC_ISSUER_DISCOVER', false),
// Public key that's used to verify the JWT token with.
// Can be the key value itself or a local 'file://public.key' reference.
'jwt_public_key' => env('OIDC_PUBLIC_KEY', null),
// OAuth2 endpoints.
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
];

View File

@ -1,6 +1,7 @@
<?php
$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);
$SAML2_SP_x509 = env('SAML2_SP_x509', false);
return [
@ -78,10 +79,11 @@ return [
// represent the requested subject.
// Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported
'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' => '',
'privateKey' => '',
'x509cert' => $SAML2_SP_x509 ?: '',
'privateKey' => env('SAML2_SP_x509_KEY', ''),
],
// Identity Provider Data that we want connect with our SP
'idp' => [
@ -147,6 +149,11 @@ return [
// Multiple forced values can be passed via a space separated array, For example:
// SAML2_IDP_AUTHNCONTEXT="urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,
// Sign requests and responses if a certificate is in use
'logoutRequestSigned' => (bool) $SAML2_SP_x509,
'logoutResponseSigned' => (bool) $SAML2_SP_x509,
'authnRequestsSigned' => (bool) $SAML2_SP_x509,
'lowercaseUrlencoding' => false,
],
],

View File

@ -28,7 +28,7 @@ class Page extends BookChild
public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at', 'priority'];
public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at', 'priority'];
protected $fillable = ['name', 'priority', 'markdown'];
protected $fillable = ['name', 'priority'];
public $textField = 'text';

View File

@ -37,7 +37,7 @@ class PageContent
*/
public function setNewHTML(string $html)
{
$html = $this->extractBase64Images($this->page, $html);
$html = $this->extractBase64ImagesFromHtml($html);
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
$this->page->markdown = '';
@ -48,6 +48,7 @@ class PageContent
*/
public function setNewMarkdown(string $markdown)
{
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
$this->page->markdown = $markdown;
$html = $this->markdownToHtml($markdown);
$this->page->html = $this->formatHtml($html);
@ -74,7 +75,7 @@ class PageContent
/**
* Convert all base64 image data to saved images.
*/
public function extractBase64Images(Page $page, string $htmlText): string
protected function extractBase64ImagesFromHtml(string $htmlText): string
{
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
return $htmlText;
@ -86,7 +87,6 @@ class PageContent
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
$imageRepo = app()->make(ImageRepo::class);
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
// Get all img elements with image data blobs
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
@ -96,7 +96,7 @@ class PageContent
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
// Validate extension
if (!in_array($extension, $allowedExtensions)) {
if (!$imageRepo->imageExtensionSupported($extension)) {
$imageNode->setAttribute('src', '');
continue;
}
@ -105,7 +105,7 @@ class PageContent
$imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
try {
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id);
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $this->page->id);
$imageNode->setAttribute('src', $image->url);
} catch (ImageUploadException $exception) {
$imageNode->setAttribute('src', '');
@ -121,6 +121,39 @@ class PageContent
return $html;
}
/**
* Convert all inline base64 content to uploaded image files.
*/
protected function extractBase64ImagesFromMarkdown(string $markdown)
{
$imageRepo = app()->make(ImageRepo::class);
$matches = [];
preg_match_all('/!\[.*?]\(.*?(data:image\/.*?)[)"\s]/', $markdown, $matches);
foreach ($matches[1] as $base64Match) {
[$dataDefinition, $base64ImageData] = explode(',', $base64Match, 2);
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
// Validate extension
if (!$imageRepo->imageExtensionSupported($extension)) {
$markdown = str_replace($base64Match, '', $markdown);
continue;
}
// Save image from data with a random name
$imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
try {
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $this->page->id);
$markdown = str_replace($base64Match, $image->url, $markdown);
} catch (ImageUploadException $exception) {
$markdown = str_replace($base64Match, '', $markdown);
}
}
return $markdown;
}
/**
* Formats a page's html to be tagged correctly within the system.
*/

View File

@ -0,0 +1,7 @@
<?php
namespace BookStack\Exceptions;
class OpenIdConnectException extends NotifyException
{
}

View File

@ -0,0 +1,165 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\FileUploadException;
use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class AttachmentApiController extends ApiController
{
protected $attachmentService;
protected $rules = [
'create' => [
'name' => 'required|min:1|max:255|string',
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required_without:link|file',
'link' => 'required_without:file|min:1|max:255|safe_url',
],
'update' => [
'name' => 'min:1|max:255|string',
'uploaded_to' => 'integer|exists:pages,id',
'file' => 'file',
'link' => 'min:1|max:255|safe_url',
],
];
public function __construct(AttachmentService $attachmentService)
{
$this->attachmentService = $attachmentService;
}
/**
* Get a listing of attachments visible to the user.
* The external property indicates whether the attachment is simple a link.
* A false value for the external property would indicate a file upload.
*/
public function list()
{
return $this->apiListingResponse(Attachment::visible(), [
'id', 'name', 'extension', 'uploaded_to', 'external', 'order', 'created_at', 'updated_at', 'created_by', 'updated_by',
]);
}
/**
* Create a new attachment in the system.
* An uploaded_to value must be provided containing an ID of the page
* that this upload will be related to.
*
* If you're uploading a file the POST data should be provided via
* a multipart/form-data type request instead of JSON.
*
* @throws ValidationException
* @throws FileUploadException
*/
public function create(Request $request)
{
$this->checkPermission('attachment-create-all');
$requestData = $this->validate($request, $this->rules['create']);
$pageId = $request->get('uploaded_to');
$page = Page::visible()->findOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
if ($request->hasFile('file')) {
$uploadedFile = $request->file('file');
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $page->id);
} else {
$attachment = $this->attachmentService->saveNewFromLink(
$requestData['name'],
$requestData['link'],
$page->id
);
}
$this->attachmentService->updateFile($attachment, $requestData);
return response()->json($attachment);
}
/**
* Get the details & content of a single attachment of the given ID.
* The attachment link or file content is provided via a 'content' property.
* For files the content will be base64 encoded.
*
* @throws FileNotFoundException
*/
public function read(string $id)
{
/** @var Attachment $attachment */
$attachment = Attachment::visible()
->with(['createdBy', 'updatedBy'])
->findOrFail($id);
$attachment->setAttribute('links', [
'html' => $attachment->htmlLink(),
'markdown' => $attachment->markdownLink(),
]);
if (!$attachment->external) {
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
$attachment->setAttribute('content', base64_encode($attachmentContents));
} else {
$attachment->setAttribute('content', $attachment->path);
}
return response()->json($attachment);
}
/**
* Update the details of a single attachment.
* As per the create endpoint, if a file is being provided as the attachment content
* the request should be formatted as a multipart/form-data request instead of JSON.
*
* @throws ValidationException
* @throws FileUploadException
*/
public function update(Request $request, string $id)
{
$requestData = $this->validate($request, $this->rules['update']);
/** @var Attachment $attachment */
$attachment = Attachment::visible()->findOrFail($id);
$page = $attachment->page;
if ($requestData['uploaded_to'] ?? false) {
$pageId = $request->get('uploaded_to');
$page = Page::visible()->findOrFail($pageId);
$attachment->uploaded_to = $requestData['uploaded_to'];
}
$this->checkOwnablePermission('page-view', $page);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-update', $attachment);
if ($request->hasFile('file')) {
$uploadedFile = $request->file('file');
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
}
$this->attachmentService->updateFile($attachment, $requestData);
return response()->json($attachment);
}
/**
* Delete an attachment of the given ID.
*
* @throws Exception
*/
public function delete(string $id)
{
/** @var Attachment $attachment */
$attachment = Attachment::visible()->findOrFail($id);
$this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment);
return response('', 204);
}
}

View File

@ -121,9 +121,9 @@ class AttachmentController extends Controller
]), 422);
}
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
$this->checkOwnablePermission('attachment-update', $attachment);
$attachment = $this->attachmentService->updateFile($attachment, [
'name' => $request->get('attachment_edit_name'),

View File

@ -43,7 +43,8 @@ class LoginController extends Controller
public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
{
$this->middleware('guest', ['only' => ['getLogin', 'login']]);
$this->middleware('guard:standard,ldap', ['only' => ['login', 'logout']]);
$this->middleware('guard:standard,ldap', ['only' => ['login']]);
$this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
$this->socialAuthService = $socialAuthService;
$this->loginService = $loginService;

View File

@ -0,0 +1,52 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Auth\Access\Oidc\OidcService;
use BookStack\Http\Controllers\Controller;
use Illuminate\Http\Request;
class OidcController extends Controller
{
protected $oidcService;
/**
* OpenIdController constructor.
*/
public function __construct(OidcService $oidcService)
{
$this->oidcService = $oidcService;
$this->middleware('guard:oidc');
}
/**
* Start the authorization login flow via OIDC.
*/
public function login()
{
$loginDetails = $this->oidcService->login();
session()->flash('oidc_state', $loginDetails['state']);
return redirect($loginDetails['url']);
}
/**
* Authorization flow redirect callback.
* Processes authorization response from the OIDC Authorization Server.
*/
public function callback(Request $request)
{
$storedState = session()->pull('oidc_state');
$responseState = $request->query('state');
if ($storedState !== $responseState) {
$this->showErrorNotification(trans('errors.oidc_fail_authed', ['system' => config('oidc.name')]));
return redirect('/login');
}
$this->oidcService->processAuthorizeResponse($request->query('code'));
return redirect()->intended();
}
}

View File

@ -4,6 +4,9 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Auth\Access\Saml2Service;
use BookStack\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Str;
class Saml2Controller extends Controller
{
@ -34,7 +37,7 @@ class Saml2Controller extends Controller
*/
public function logout()
{
$logoutDetails = $this->samlService->logout();
$logoutDetails = $this->samlService->logout(auth()->user());
if ($logoutDetails['id']) {
session()->flash('saml2_logout_request_id', $logoutDetails['id']);
@ -68,15 +71,59 @@ class Saml2Controller extends Controller
}
/**
* Assertion Consumer Service.
* Processes the SAML response from the IDP.
* Assertion Consumer Service start URL. Takes the SAMLResponse from the IDP.
* Due to being an external POST request, we likely won't have context of the
* current user session due to lax cookies. To work around this we store the
* SAMLResponse data and redirect to the processAcs endpoint for the actual
* processing of the request with proper context of the user session.
*/
public function acs()
public function startAcs(Request $request)
{
$requestId = session()->pull('saml2_request_id', null);
// Note: This is a bit of a hack to prevent a session being stored
// on the response of this request. Within Laravel7+ this could instead
// be done via removing the StartSession middleware from the route.
config()->set('session.driver', 'array');
$user = $this->samlService->processAcsResponse($requestId);
if ($user === null) {
$samlResponse = $request->get('SAMLResponse', null);
if (empty($samlResponse)) {
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
return redirect('/login');
}
$acsId = Str::random(16);
$cacheKey = 'saml2_acs:' . $acsId;
cache()->set($cacheKey, encrypt($samlResponse), 10);
return redirect()->guest('/saml2/acs?id=' . $acsId);
}
/**
* Assertion Consumer Service process endpoint.
* Processes the SAML response from the IDP with context of the current session.
* Takes the SAML request from the cache, added by the startAcs method above.
*/
public function processAcs(Request $request)
{
$acsId = $request->get('id', null);
$cacheKey = 'saml2_acs:' . $acsId;
$samlResponse = null;
try {
$samlResponse = decrypt(cache()->pull($cacheKey));
} catch (\Exception $exception) {
}
$requestId = session()->pull('saml2_request_id', 'unset');
if (empty($acsId) || empty($samlResponse)) {
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
return redirect('/login');
}
$user = $this->samlService->processAcsResponse($requestId, $samlResponse);
if (is_null($user)) {
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
return redirect('/login');

View File

@ -84,7 +84,7 @@ class UserController extends Controller
if ($authMethod === 'standard' && !$sendInvite) {
$validationRules['password'] = 'required|min:6';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2') {
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') {
$validationRules['external_auth_id'] = 'required';
}
$this->validate($request, $validationRules);
@ -93,7 +93,7 @@ class UserController extends Controller
if ($authMethod === 'standard') {
$user->password = bcrypt($request->get('password', Str::random(32)));
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2') {
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') {
$user->external_auth_id = $request->get('external_auth_id');
}

View File

@ -15,6 +15,7 @@ class Localization
/**
* Map of BookStack locale names to best-estimate system locale names.
* Locales can often be found by running `locale -a` on a linux system.
*/
protected $localeMap = [
'ar' => 'ar',
@ -27,6 +28,7 @@ class Localization
'en' => 'en_GB',
'es' => 'es_ES',
'es_AR' => 'es_AR',
'et' => 'et_EE',
'fr' => 'fr_FR',
'he' => 'he_IL',
'hr' => 'hr_HR',

View File

@ -13,6 +13,7 @@ use BookStack\Exceptions\WhoopsBookStackPrettyHandler;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use BookStack\Util\CspService;
use GuzzleHttp\Client;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Blade;
@ -21,6 +22,7 @@ use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
use Psr\Http\Client\ClientInterface as HttpClientInterface;
use Whoops\Handler\HandlerInterface;
class AppServiceProvider extends ServiceProvider
@ -82,5 +84,11 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(CspService::class, function ($app) {
return new CspService();
});
$this->app->bind(HttpClientInterface::class, function ($app) {
return new Client([
'timeout' => 3,
]);
});
}
}

View File

@ -4,8 +4,8 @@ namespace BookStack\Providers;
use BookStack\Api\ApiTokenGuard;
use BookStack\Auth\Access\ExternalBaseUserProvider;
use BookStack\Auth\Access\Guards\AsyncExternalBaseSessionGuard;
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;
@ -37,10 +37,10 @@ class AuthServiceProvider extends ServiceProvider
);
});
Auth::extend('saml2-session', function ($app, $name, array $config) {
Auth::extend('async-external-session', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
return new Saml2SessionGuard(
return new AsyncExternalBaseSessionGuard(
$name,
$provider,
$app['session.store'],

View File

@ -2,24 +2,37 @@
namespace BookStack\Uploads;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int id
* @property string name
* @property string path
* @property string extension
* @property ?Page page
* @property bool external
* @property int $id
* @property string $name
* @property string $path
* @property string $extension
* @property ?Page $page
* @property bool $external
* @property int $uploaded_to
* @property User $updatedBy
* @property User $createdBy
*
* @method static Entity|Builder visible()
*/
class Attachment extends Model
{
use HasCreatorAndUpdater;
protected $fillable = ['name', 'order'];
protected $hidden = ['path', 'page'];
protected $casts = [
'external' => 'bool',
];
/**
* Get the downloadable file name for this upload.
@ -70,4 +83,19 @@ class Attachment extends Model
{
return '[' . $this->name . '](' . $this->getUrl() . ')';
}
/**
* Scope the query to those attachments that are visible based upon related page permissions.
*/
public function scopeVisible(): Builder
{
$permissionService = app()->make(PermissionService::class);
return $permissionService->filterRelatedEntity(
Page::class,
Attachment::query(),
'attachments',
'uploaded_to'
);
}
}

View File

@ -78,18 +78,18 @@ class AttachmentService
*
* @throws FileUploadException
*/
public function saveNewUpload(UploadedFile $uploadedFile, int $page_id): Attachment
public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($uploadedFile);
$largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $page_id)->max('order');
$largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $pageId)->max('order');
/** @var Attachment $attachment */
$attachment = Attachment::query()->forceCreate([
'name' => $attachmentName,
'path' => $attachmentPath,
'extension' => $uploadedFile->getClientOriginalExtension(),
'uploaded_to' => $page_id,
'uploaded_to' => $pageId,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1,
@ -159,18 +159,20 @@ class AttachmentService
public function updateFile(Attachment $attachment, array $requestData): Attachment
{
$attachment->name = $requestData['name'];
$link = trim($requestData['link'] ?? '');
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
$attachment->path = $requestData['link'];
if (!empty($link)) {
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
$attachment->external = true;
$attachment->extension = '';
}
$attachment->path = $requestData['link'];
}
$attachment->save();
return $attachment;
return $attachment->refresh();
}
/**
@ -180,13 +182,10 @@ class AttachmentService
*/
public function deleteFile(Attachment $attachment)
{
if ($attachment->external) {
$attachment->delete();
return;
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
}
$this->deleteFileInStorage($attachment);
$attachment->delete();
}

View File

@ -16,6 +16,8 @@ class ImageRepo
protected $restrictionService;
protected $page;
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
/**
* ImageRepo constructor.
*/
@ -31,6 +33,14 @@ class ImageRepo
$this->page = $page;
}
/**
* Check if the given image extension is supported by BookStack.
*/
public function imageExtensionSupported(string $extension): bool
{
return in_array(trim($extension, '. \t\n\r\0\x0B'), static::$supportedExtensions);
}
/**
* Get an image with the given id.
*/

View File

@ -25,8 +25,10 @@
"league/commonmark": "^1.5",
"league/flysystem-aws-s3-v3": "^1.0.29",
"league/html-to-markdown": "^5.0.0",
"league/oauth2-client": "^2.6",
"nunomaduro/collision": "^3.1",
"onelogin/php-saml": "^4.0",
"phpseclib/phpseclib": "~3.0",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1.6",
"socialiteproviders/discord": "^4.1",

196
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d59a665fcd692fc0ddf12e7e4f96d4f1",
"content-hash": "fc6d8f731e3975127a9101802cc4bb3a",
"packages": [
{
"name": "aws/aws-crt-php",
@ -58,16 +58,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.198.5",
"version": "3.198.6",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "ec63e1ad1b30689e530089e4c9cb18f2ef5c290b"
"reference": "821b8db50dd39be8ec94f286050a500b5f8a0142"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ec63e1ad1b30689e530089e4c9cb18f2ef5c290b",
"reference": "ec63e1ad1b30689e530089e4c9cb18f2ef5c290b",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/821b8db50dd39be8ec94f286050a500b5f8a0142",
"reference": "821b8db50dd39be8ec94f286050a500b5f8a0142",
"shasum": ""
},
"require": {
@ -143,9 +143,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.198.5"
"source": "https://github.com/aws/aws-sdk-php/tree/3.198.6"
},
"time": "2021-10-14T18:15:37+00:00"
"time": "2021-10-15T18:38:53+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -2371,6 +2371,76 @@
},
"time": "2021-08-15T23:05:49+00:00"
},
{
"name": "league/oauth2-client",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth2-client.git",
"reference": "badb01e62383430706433191b82506b6df24ad98"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/badb01e62383430706433191b82506b6df24ad98",
"reference": "badb01e62383430706433191b82506b6df24ad98",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"paragonie/random_compat": "^1 || ^2 || ^9.99",
"php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"mockery/mockery": "^1.3",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpunit/phpunit": "^5.7 || ^6.0 || ^9.3",
"squizlabs/php_codesniffer": "^2.3 || ^3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\OAuth2\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alex Bilbie",
"email": "hello@alexbilbie.com",
"homepage": "http://www.alexbilbie.com",
"role": "Developer"
},
{
"name": "Woody Gilk",
"homepage": "https://github.com/shadowhand",
"role": "Contributor"
}
],
"description": "OAuth 2.0 Client Library",
"keywords": [
"Authentication",
"SSO",
"authorization",
"identity",
"idp",
"oauth",
"oauth2",
"single sign on"
],
"support": {
"issues": "https://github.com/thephpleague/oauth2-client/issues",
"source": "https://github.com/thephpleague/oauth2-client/tree/2.6.0"
},
"time": "2020-10-28T02:03:40+00:00"
},
{
"name": "monolog/monolog",
"version": "2.3.5",
@ -3199,6 +3269,117 @@
],
"time": "2021-08-28T21:27:29+00:00"
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.10",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "62fcc5a94ac83b1506f52d7558d828617fac9187"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/62fcc5a94ac83b1506f52d7558d828617fac9187",
"reference": "62fcc5a94ac83b1506f52d7558d828617fac9187",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1|^2",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": ">=5.6.1"
},
"require-dev": {
"phing/phing": "~2.7",
"phpunit/phpunit": "^5.7|^6.0|^9.4",
"squizlabs/php_codesniffer": "~2.0"
},
"suggest": {
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
},
"type": "library",
"autoload": {
"files": [
"phpseclib/bootstrap.php"
],
"psr-4": {
"phpseclib3\\": "phpseclib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jim Wigginton",
"email": "terrafrost@php.net",
"role": "Lead Developer"
},
{
"name": "Patrick Monnerat",
"email": "pm@datasphere.ch",
"role": "Developer"
},
{
"name": "Andreas Fischer",
"email": "bantu@phpbb.com",
"role": "Developer"
},
{
"name": "Hans-Jürgen Petrich",
"email": "petrich@tronic-media.com",
"role": "Developer"
},
{
"name": "Graham Campbell",
"email": "graham@alt-three.com",
"role": "Developer"
}
],
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
"homepage": "http://phpseclib.sourceforge.net",
"keywords": [
"BigInteger",
"aes",
"asn.1",
"asn1",
"blowfish",
"crypto",
"cryptography",
"encryption",
"rsa",
"security",
"sftp",
"signature",
"signing",
"ssh",
"twofish",
"x.509",
"x509"
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.10"
},
"funding": [
{
"url": "https://github.com/terrafrost",
"type": "github"
},
{
"url": "https://www.patreon.com/phpseclib",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
"type": "tidelift"
}
],
"time": "2021-08-16T04:24:45+00:00"
},
{
"name": "pragmarx/google2fa",
"version": "8.0.0",
@ -9289,6 +9470,7 @@
"type": "github"
}
],
"abandoned": true,
"time": "2020-09-28T06:45:17+00:00"
},
{

View File

@ -0,0 +1,5 @@
{
"name": "My uploaded attachment",
"uploaded_to": 8,
"link": "https://link.example.com"
}

View File

@ -0,0 +1,5 @@
{
"name": "My updated attachment",
"uploaded_to": 4,
"link": "https://link.example.com/updated"
}

View File

@ -0,0 +1,12 @@
{
"id": 5,
"name": "My uploaded attachment",
"extension": "",
"uploaded_to": 8,
"external": true,
"order": 2,
"created_by": 1,
"updated_by": 1,
"created_at": "2021-10-20 06:35:46",
"updated_at": "2021-10-20 06:35:46"
}

View File

@ -0,0 +1,29 @@
{
"data": [
{
"id": 3,
"name": "datasheet.pdf",
"extension": "pdf",
"uploaded_to": 8,
"external": false,
"order": 1,
"created_at": "2021-10-11 06:18:49",
"updated_at": "2021-10-20 06:31:10",
"created_by": 1,
"updated_by": 1
},
{
"id": 4,
"name": "Cat reference",
"extension": "",
"uploaded_to": 9,
"external": true,
"order": 1,
"created_at": "2021-10-20 06:30:11",
"updated_at": "2021-10-20 06:30:11",
"created_by": 1,
"updated_by": 1
}
],
"total": 2
}

View File

@ -0,0 +1,25 @@
{
"id": 5,
"name": "My link attachment",
"extension": "",
"uploaded_to": 4,
"external": true,
"order": 2,
"created_by": {
"id": 1,
"name": "Admin",
"slug": "admin"
},
"updated_by": {
"id": 1,
"name": "Admin",
"slug": "admin"
},
"created_at": "2021-10-20 06:35:46",
"updated_at": "2021-10-20 06:37:11",
"links": {
"html": "<a target=\"_blank\" href=\"https://bookstack.local/attachments/5\">My updated attachment</a>",
"markdown": "[My updated attachment](https://bookstack.local/attachments/5)"
},
"content": "https://link.example.com/updated"
}

View File

@ -0,0 +1,12 @@
{
"id": 5,
"name": "My updated attachment",
"extension": "",
"uploaded_to": 4,
"external": true,
"order": 2,
"created_by": 1,
"updated_by": 1,
"created_at": "2021-10-20 06:35:46",
"updated_at": "2021-10-20 06:37:11"
}

View File

@ -199,4 +199,5 @@ These are the great open-source projects used to help build BookStack:
* [League/Flysystem](https://flysystem.thephpleague.com)
* [StyleCI](https://styleci.io/)
* [pragmarx/google2fa](https://github.com/antonioribeiro/google2fa)
* [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode)
* [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode)
* [phpseclib](https://github.com/phpseclib/phpseclib)

4
resources/icons/oidc.svg Normal file
View File

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'تعذر العثور على عنوان بريد إلكتروني، لهذا المستخدم، في البيانات المقدمة من نظام المصادقة الخارجي',
'saml_invalid_response_id' => 'لم يتم التعرف على الطلب من نظام التوثيق الخارجي من خلال عملية تبدأ بهذا التطبيق. العودة بعد تسجيل الدخول يمكن أن يسبب هذه المشكلة.',
'saml_fail_authed' => 'تسجيل الدخول باستخدام :system فشل، النظام لم يوفر التفويض الناجح',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'لم يتم تعريف أي إجراء',
'social_login_bad_response' => "حصل خطأ خلال تسجيل الدخول باستخدام :socialAccount \n:error",
'social_account_in_use' => 'حساب :socialAccount قيد الاستخدام حالياً, الرجاء محاولة الدخول باستخدام خيار :socialAccount.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Не успяхме да намерим емейл адрес, за този потребител, от информацията предоставена от външната система',
'saml_invalid_response_id' => 'Заявката от външната система не е разпознат от процеса започнат от това приложение. Връщането назад след влизане може да породи този проблем.',
'saml_fail_authed' => 'Влизането чрез :system не беше успешно, системата не успя да оторизира потребителя',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Действието не беше дефинирано',
'social_login_bad_response' => "Възникна грешка по време на :socialAccount login: \n:error",
'social_account_in_use' => 'Този :socialAccount вече е използван. Опитайте се да влезете чрез опцията за :socialAccount.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'E-mail adresa za ovog korisnika nije nađena u podacima dobijenim od eksternog autentifikacijskog sistema',
'saml_invalid_response_id' => 'Proces, koji je pokrenula ova aplikacija, nije prepoznao zahtjev od eksternog sistema za autentifikaciju. Navigacija nazad nakon prijave može uzrokovati ovaj problem.',
'saml_fail_authed' => 'Prijava koristeći :system nije uspjela, sistem nije obezbijedio uspješnu autorizaciju',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Nema definisane akcije',
'social_login_bad_response' => "Došlo je do greške prilikom prijave preko :socialAccount :\n:error",
'social_account_in_use' => 'Ovaj :socialAccount račun se već koristi, pokušajte se prijaviti putem :socialAccount opcije.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'No s\'ha pogut trobar cap adreça electrònica, per a aquest usuari, en les dades proporcionades pel sistema d\'autenticació extern',
'saml_invalid_response_id' => 'La petició del sistema d\'autenticació extern no és reconeguda per un procés iniciat per aquesta aplicació. Aquest problema podria ser causat per navegar endarrere després d\'iniciar la sessió.',
'saml_fail_authed' => 'L\'inici de sessió fent servir :system ha fallat, el sistema no ha proporcionat una autorització satisfactòria',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'No hi ha cap acció definida',
'social_login_bad_response' => "S'ha rebut un error mentre s'iniciava la sessió amb :socialAccount: \n:error",
'social_account_in_use' => 'Aquest compte de :socialAccount ja està en ús, proveu d\'iniciar la sessió mitjançant l\'opció de :socialAccount.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Nelze najít e-mailovou adresu pro tohoto uživatele v datech poskytnutých externím přihlašovacím systémem',
'saml_invalid_response_id' => 'Požadavek z externího ověřovacího systému nebyl rozpoznám procesem, který tato aplikace spustila. Tento problém může způsobit stisknutí tlačítka Zpět po přihlášení.',
'saml_fail_authed' => 'Přihlášení pomocí :system selhalo, systém neposkytl úspěšnou autorizaci',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Nebyla zvolena žádá akce',
'social_login_bad_response' => "Nastala chyba během přihlašování přes :socialAccount \n:error",
'social_account_in_use' => 'Tento účet na :socialAccount se již používá. Pokuste se s ním přihlásit volbou Přihlásit přes :socialAccount.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Kunne ikke finde en e-mail-adresse for denne bruger i de data, der leveres af det eksterne godkendelsessystem',
'saml_invalid_response_id' => 'Anmodningen fra det eksterne godkendelsessystem genkendes ikke af en proces, der er startet af denne applikation. Navigering tilbage efter et login kan forårsage dette problem.',
'saml_fail_authed' => 'Login ved hjælp af :system failed, systemet har ikke givet tilladelse',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Ingen handling er defineret',
'social_login_bad_response' => "Der opstod en fejl under :socialAccount login:\n:error",
'social_account_in_use' => 'Denne :socialAccount konto er allerede i brug, prøv at logge ind med :socialAccount loginmetoden.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'Hebraisk',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem zur Verfügung gestellten Daten gefunden werden',
'saml_invalid_response_id' => 'Die Anfrage vom externen Authentifizierungssystem wird von einem von dieser Anwendung gestarteten Prozess nicht erkannt. Das Zurückgehen nach einem Login könnte dieses Problem verursachen.',
'saml_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System konnte keine erfolgreiche Autorisierung bereitstellen',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Es ist keine Aktion definiert',
'social_login_bad_response' => "Fehler bei der :socialAccount-Anmeldung: \n:error",
'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.',

View File

@ -251,6 +251,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'Hebräisch',
'hr' => 'Hrvatski',

View File

@ -48,8 +48,8 @@ return [
'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',
'mfa_setup_method_notification' => 'Multi-Faktor-Methode erfolgreich konfiguriert',
'mfa_remove_method_notification' => 'Multi-Faktor-Methode erfolgreich entfernt',
// Other
'commented_on' => 'kommentiert',

View File

@ -76,37 +76,37 @@ return [
'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_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' => '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_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' => '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.',
'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.',
];

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Es konnte keine E-Mail-Adresse für diesen Benutzer in den vom externen Authentifizierungssystem zur Verfügung gestellten Daten gefunden werden',
'saml_invalid_response_id' => 'Die Anfrage vom externen Authentifizierungssystem wird von einem von dieser Anwendung gestarteten Prozess nicht erkannt. Das Zurückgehen nach einem Login könnte dieses Problem verursachen.',
'saml_fail_authed' => 'Anmeldung mit :system fehlgeschlagen, System konnte keine erfolgreiche Autorisierung bereitstellen',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Es ist keine Aktion definiert',
'social_login_bad_response' => "Fehler bei :socialAccount Login: \n:error",
'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melde dich mit dem :socialAccount-Konto an.',
@ -84,7 +88,7 @@ return [
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Du angefordert hast, wurde nicht gefunden.',
'sorry_page_not_found_permission_warning' => 'Wenn du erwartet hast, dass diese Seite existiert, hast du möglicherweise nicht die Berechtigung, sie anzuzeigen.',
'image_not_found' => 'Bild nicht gefunden',
'image_not_found_subtitle' => 'Entschuldigung. Das Bild, die Sie angefordert haben, wurde nicht gefunden.',
'image_not_found_subtitle' => 'Entschuldigung. Das angeforderte Bild wurde nicht gefunden.',
'image_not_found_details' => 'Wenn Sie erwartet haben, dass dieses Bild existiert, könnte es gelöscht worden sein.',
'return_home' => 'Zurück zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten',

View File

@ -122,7 +122,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
'audit_table_user' => 'Benutzer',
'audit_table_event' => 'Ereignis',
'audit_table_related' => 'Verknüpfter Eintrag oder Detail',
'audit_table_ip' => 'IP Address',
'audit_table_ip' => 'IP Adresse',
'audit_table_date' => 'Aktivitätsdatum',
'audit_date_from' => 'Zeitraum von',
'audit_date_to' => 'Zeitraum bis',
@ -251,6 +251,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -15,7 +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.',
'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.',
@ -99,7 +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.',
'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.',

View File

@ -23,6 +23,10 @@ return [
'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',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_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.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',
'saml_invalid_response_id' => 'La solicitud del sistema de autenticación externo no está reconocida por un proceso iniciado por esta aplicación. Navegar hacia atrás después de un inicio de sesión podría causar este problema.',
'saml_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',
'oidc_already_logged_in' => 'Ya tenías la sesión iniciada',
'oidc_user_not_registered' => 'El usuario :name no está registrado y el registro automático está deshabilitado',
'oidc_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',
'oidc_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',
'social_no_action_defined' => 'Acción no definida',
'social_login_bad_response' => "Se ha recibido un error durante el acceso con :socialAccount error: \n:error",
'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente acceder a través de la opción :socialAccount .',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',
'saml_invalid_response_id' => 'La solicitud del sistema de autenticación externo no está reconocida por un proceso iniciado por esta aplicación. Navegar hacia atrás después de un inicio de sesión podría causar este problema.',
'saml_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',
'oidc_already_logged_in' => 'Ya tenías la sesión iniciada',
'oidc_user_not_registered' => 'El usuario :name no está registrado y el registro automático está deshabilitado',
'oidc_no_email_address' => 'No se pudo encontrar una dirección de correo electrónico, para este usuario, en los datos proporcionados por el sistema de autenticación externo',
'oidc_fail_authed' => 'El inicio de sesión con :system falló, el sistema no proporcionó una autorización correcta',
'social_no_action_defined' => 'Acción no definida',
'social_login_bad_response' => "SE recibió un Error durante el acceso con :socialAccount : \n:error",
'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente loguearse a través de la opcón :socialAccount .',

View File

@ -249,6 +249,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -0,0 +1,57 @@
<?php
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
return [
// Pages
'page_create' => 'lisas lehe',
'page_create_notification' => 'Leht on lisatud',
'page_update' => 'muutis lehte',
'page_update_notification' => 'Leht on muudetud',
'page_delete' => 'kustutas lehe',
'page_delete_notification' => 'Leht on kustutatud',
'page_restore' => 'taastas lehe',
'page_restore_notification' => 'Leht on taastatud',
'page_move' => 'liigutas lehte',
// Chapters
'chapter_create' => 'lisas peatüki',
'chapter_create_notification' => 'Peatükk on lisatud',
'chapter_update' => 'muutis peatükki',
'chapter_update_notification' => 'Peatükk on muudetud',
'chapter_delete' => 'kustutas peatüki',
'chapter_delete_notification' => 'Peatükk on kustutatud',
'chapter_move' => 'liigutas peatükki',
// Books
'book_create' => 'lisas raamatu',
'book_create_notification' => 'Raamat on lisatud',
'book_update' => 'muutis raamatut',
'book_update_notification' => 'Raamat on muudetud',
'book_delete' => 'kustutas raamatu',
'book_delete_notification' => 'Raamat on kustutatud',
'book_sort' => 'sorteeris raamatut',
'book_sort_notification' => 'Raamat on sorteeritud',
// Bookshelves
'bookshelf_create' => 'lisas riiuli',
'bookshelf_create_notification' => 'Riiul on lisatud',
'bookshelf_update' => 'muutis riiulit',
'bookshelf_update_notification' => 'Riiul on muudetud',
'bookshelf_delete' => 'kustutas riiuli',
'bookshelf_delete_notification' => 'Riiul on kustutatud',
// Favourites
'favourite_add_notification' => '":name" lisati su lemmikute hulka',
'favourite_remove_notification' => '":name" eemaldati su lemmikute hulgast',
// MFA
'mfa_setup_method_notification' => 'Mitmeastmeline autentimine seadistatud',
'mfa_remove_method_notification' => 'Mitmeastmeline autentimine eemaldatud',
// Other
'commented_on' => 'kommenteeris lehte',
'permissions_update' => 'muutis õiguseid',
];

112
resources/lang/et/auth.php Normal file
View File

@ -0,0 +1,112 @@
<?php
/**
* Authentication Language Lines
* The following language lines are used during authentication for various
* messages that we need to display to the user.
*/
return [
'failed' => 'Kasutajanimi ja parool ei klapi.',
'throttle' => 'Liiga palju sisselogimiskatseid. Proovi uuesti :seconds sekundi pärast.',
// Login & Register
'sign_up' => 'Registreeru',
'log_in' => 'Logi sisse',
'log_in_with' => 'Logi sisse :socialDriver abil',
'sign_up_with' => 'Registreeru :socialDriver abil',
'logout' => 'Logi välja',
'name' => 'Nimi',
'username' => 'Kasutajanimi',
'email' => 'E-post',
'password' => 'Parool',
'password_confirm' => 'Kinnita parool',
'password_hint' => 'Peab olema rohkem kui 7 tähemärki',
'forgot_password' => 'Unustasid parooli?',
'remember_me' => 'Jäta mind meelde',
'ldap_email_hint' => 'Sisesta kasutajakonto e-posti aadress.',
'create_account' => 'Loo konto',
'already_have_account' => 'Kasutajakonto juba olemas?',
'dont_have_account' => 'Sul ei ole veel kontot?',
'social_login' => 'Social Login',
'social_registration' => 'Social Registration',
'social_registration_text' => 'Registreeru ja logi sisse välise teenuse kaudu.',
'register_thanks' => 'Aitäh, et registreerusid!',
'register_confirm' => 'Vaata oma postkasti ja klõpsa kinnitusnupul, et rakendusele :appName ligi pääseda.',
'registrations_disabled' => 'Registreerumine on hetkel keelatud',
'registration_email_domain_invalid' => 'Sellel e-posti domeenil ei ole rakendusele ligipääsu',
'register_success' => 'Aitäh, et registreerusid! Oled nüüd sisse logitud.',
// Password Reset
'reset_password' => 'Lähtesta parool',
'reset_password_send_instructions' => 'Siseta oma e-posti aadress ning sulle saadetakse link parooli lähtestamiseks.',
'reset_password_send_button' => 'Saada lähtestamise link',
'reset_password_sent' => 'Kui süsteemis leidub e-posti aadress :email, saadetakse sinna link parooli lähtestamiseks.',
'reset_password_success' => 'Sinu parool on edukalt lähtestatud.',
'email_reset_subject' => 'Lähtesta oma :appName parool',
'email_reset_text' => 'Said selle e-kirja, sest meile laekus soov sinu konto parooli lähtestamiseks.',
'email_reset_not_requested' => 'Kui sa ei soovinud parooli lähtestada, ei pea sa rohkem midagi tegema.',
// Email Confirmation
'email_confirm_subject' => 'Kinnita oma :appName konto e-posti aadress',
'email_confirm_greeting' => 'Aitäh, et liitusid rakendusega :appName!',
'email_confirm_text' => 'Palun kinnita oma e-posti aadress, klõpsates alloleval nupul:',
'email_confirm_action' => 'Kinnita e-posti aadress',
'email_confirm_send_error' => 'E-posti aadressi kinnitamine on vajalik, aga e-kirja saatmine ebaõnnestus. Võta ühendust administraatoriga.',
'email_confirm_success' => 'Sinu e-posti aadress on kinnitatud!',
'email_confirm_resent' => 'Kinnituskiri on saadetud, vaata oma postkasti.',
'email_not_confirmed' => 'E-posti aadress ei ole kinnitatud',
'email_not_confirmed_text' => 'Sinu e-posti aadress ei ole veel kinnitatud.',
'email_not_confirmed_click_link' => 'Klõpsa lingil e-kirjas, mis saadeti sulle pärast registreerumist.',
'email_not_confirmed_resend' => 'Kui sa ei leia e-kirja, siis saad alloleva vormi abil selle uuesti saata.',
'email_not_confirmed_resend_button' => 'Saada kinnituskiri uuesti',
// User Invite
'user_invite_email_subject' => 'Sind on kutsutud liituma rakendusega :appName!',
'user_invite_email_greeting' => 'Sulle on loodud kasutajakonto rakenduses :appName.',
'user_invite_email_text' => 'Vajuta allolevale nupule, et seada parool ja ligipääs saada:',
'user_invite_email_action' => 'Sea konto parool',
'user_invite_page_welcome' => 'Tere tulemast rakendusse :appName!',
'user_invite_page_text' => 'Registreerumise lõpetamiseks ja ligipääsu saamiseks pead seadma parooli, millega edaspidi rakendusse sisse logid.',
'user_invite_page_confirm_button' => 'Kinnita parool',
'user_invite_success' => 'Parool seatud, sul on nüüd ligipääs!',
// Multi-factor Authentication
'mfa_setup' => 'Seadista mitmeastmeline autentimine',
'mfa_setup_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',
'mfa_setup_configured' => 'Juba seadistatud',
'mfa_setup_reconfigure' => 'Seadista ümber',
'mfa_setup_remove_confirmation' => 'Kas oled kindel, et soovid selle mitmeastmelise autentimise meetodi eemaldada?',
'mfa_setup_action' => 'Seadista',
'mfa_backup_codes_usage_limit_warning' => 'Sul on vähem kui 5 varukoodi järel. Genereeri ja hoiusta uus komplekt enne, kui nad otsa saavad, et vältida oma kasutajakontole ligipääsu kaotamist.',
'mfa_option_totp_title' => 'Mobiilirakendus',
'mfa_option_totp_desc' => 'Mitmeastmelise autentimise kasutamiseks on sul vaja TOTP-toega mobiilirakendust, nagu Google Authenticator, Authy või Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Varukoodid',
'mfa_option_backup_codes_desc' => 'Hoiusta kindlas kohas komplekt ühekordseid varukoode, millega saad oma isikut tuvastada.',
'mfa_gen_confirm_and_enable' => 'Kinnita ja lülita sisse',
'mfa_gen_backup_codes_title' => 'Varukoodide seadistamine',
'mfa_gen_backup_codes_desc' => 'Hoiusta allolevad koodid turvalises kohas. Saad neid kasutada sisselogimisel sekundaarse autentimismeetodina.',
'mfa_gen_backup_codes_download' => 'Laadi koodid alla',
'mfa_gen_backup_codes_usage_warning' => 'Igat koodi saab ainult ühe korra kasutada',
'mfa_gen_totp_title' => 'Mobiilirakenduse seadistamine',
'mfa_gen_totp_desc' => 'Mitmeastmelise autentimise kasutamiseks on sul vaja TOTP-toega mobiilirakendust, nagu Google Authenticator, Authy või Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Alustamiseks skaneeri allolevat QR-koodi oma eelistatud rakendusega.',
'mfa_gen_totp_verify_setup' => 'Kontrolli seadistust',
'mfa_gen_totp_verify_setup_desc' => 'Veendu, et kõik toimib korrektselt, sisestades oma rakenduse genereeritud koodi allolevasse tekstikasti:',
'mfa_gen_totp_provide_code_here' => 'Sisesta rakenduse genereeritud kood siia',
'mfa_verify_access' => 'Kinnita ligipääs',
'mfa_verify_access_desc' => 'Sinu konto nõuab ligipääsuks täiendava kinnitusmeetodi abil oma isiku tuvastamist. Jätkamiseks vali üks järgnevatest meetoditest.',
'mfa_verify_no_methods' => 'Ühtegi meetodit pole seadistatud',
'mfa_verify_no_methods_desc' => 'Sinu kontole pole lisatud ühtegi mitmeastmelise autentimise meetodit. Ligipääsu saamiseks pead seadistama vähemalt ühe meetodi.',
'mfa_verify_use_totp' => 'Tuvasta mobiilirakendusega',
'mfa_verify_use_backup_codes' => 'Tuvasta varukoodiga',
'mfa_verify_backup_code' => 'Varukood',
'mfa_verify_backup_code_desc' => 'Sisesta allpool üks oma järelejäänud varukoodidest:',
'mfa_verify_backup_code_enter_here' => 'Sisesta varukood siia',
'mfa_verify_totp_desc' => 'Sisesta oma mobiilirakenduse poolt genereeritud kood allpool:',
'mfa_setup_login_notification' => 'Mitmeastmeline autentimine seadistatud. Logi nüüd uuesti sisse, kasutades seadistatud meetodit.',
];

View File

@ -0,0 +1,95 @@
<?php
/**
* Common elements found throughout many areas of BookStack.
*/
return [
// Buttons
'cancel' => 'Tühista',
'confirm' => 'Kinnita',
'back' => 'Tagasi',
'save' => 'Salvesta',
'continue' => 'Jätka',
'select' => 'Vali',
'toggle_all' => 'Vaheta kõik',
'more' => 'Rohkem',
// Form Labels
'name' => 'Pealkiri',
'description' => 'Kirjeldus',
'role' => 'Roll',
'cover_image' => 'Kaanepilt',
'cover_image_description' => 'See pilt peaks olema umbes 440x250 pikslit.',
// Actions
'actions' => 'Tegevused',
'view' => 'Vaata',
'view_all' => 'Vaata kõiki',
'create' => 'Lisa',
'update' => 'Uuenda',
'edit' => 'Muuda',
'sort' => 'Sorteeri',
'move' => 'Liiguta',
'copy' => 'Kopeeri',
'reply' => 'Vasta',
'delete' => 'Kustuta',
'delete_confirm' => 'Kinnita kustutamine',
'search' => 'Otsi',
'search_clear' => 'Tühjenda otsing',
'reset' => 'Taasta',
'remove' => 'Eemalda',
'add' => 'Lisa',
'configure' => 'Seadista',
'fullscreen' => 'Täisekraan',
'favourite' => 'Lemmik',
'unfavourite' => 'Eemalda lemmik',
'next' => 'Järgmine',
'previous' => 'Eelmine',
// Sort Options
'sort_options' => 'Sorteerimise valikud',
'sort_direction_toggle' => 'Sorteerimise suund',
'sort_ascending' => 'Sorteeri kasvavalt',
'sort_descending' => 'Sorteeri kahanevalt',
'sort_name' => 'Pealkiri',
'sort_default' => 'Vaikimisi',
'sort_created_at' => 'Loomise aeg',
'sort_updated_at' => 'Muutmise aeg',
// Misc
'deleted_user' => 'Kustutatud kasutaja',
'no_activity' => 'Pole tegevusi, mida näidata',
'no_items' => 'Ühtegi elementi pole',
'back_to_top' => 'Tagasi üles',
'skip_to_main_content' => 'Otse põhisisu juurde',
'toggle_details' => 'Näita detaile',
'toggle_thumbnails' => 'Näita eelvaateid',
'details' => 'Detailid',
'grid_view' => 'Tabelivaade',
'list_view' => 'Loendivaade',
'default' => 'Vaikimisi',
'breadcrumb' => 'Jäljerida',
// Header
'header_menu_expand' => 'Laienda päisemenüü',
'profile_menu' => 'Profiilimenüü',
'view_profile' => 'Vaata profiili',
'edit_profile' => 'Muuda profiili',
'dark_mode' => 'Tume režiim',
'light_mode' => 'Hele režiim',
// Layout tabs
'tab_info' => 'Info',
'tab_info_label' => 'Tab: Show Secondary Information',
'tab_content' => 'Sisu',
'tab_content_label' => 'Tab: Show Primary Content',
// Email Content
'email_action_help' => 'Kui sul on probleeme ":actionText" nupu vajutamisega, kopeeri allolev URL oma veebilehitsejasse:',
'email_rights' => 'Kõik õigused kaitstud',
// Footer Link Options
// Not directly used but available for convenience to users.
'privacy_policy' => 'Privaatsus',
'terms_of_service' => 'Kasutustingimused',
];

View File

@ -0,0 +1,34 @@
<?php
/**
* Text used in custom JavaScript driven components.
*/
return [
// Image Manager
'image_select' => 'Pildifaili valik',
'image_all' => 'Kõik',
'image_all_title' => 'Vaata kõiki pildifaile',
'image_book_title' => 'Vaata sellesse raamatusse laaditud pildifaile',
'image_page_title' => 'Vaata sellele lehele laaditud pildifaile',
'image_search_hint' => 'Otsi pildifaili nime järgi',
'image_uploaded' => 'Üles laaditud :uploadedDate',
'image_load_more' => 'Lae rohkem',
'image_image_name' => 'Pildifaili nimi',
'image_delete_used' => 'Seda pildifaili kasutavad järgmised lehed.',
'image_delete_confirm_text' => 'Kas oled kindel, et soovid selle pildifaili kustutada?',
'image_select_image' => 'Vali pildifail',
'image_dropzone' => 'Üleslaadimiseks lohista pildid või klõpsa siin',
'images_deleted' => 'Pildifailid kustutatud',
'image_preview' => 'Pildi eelvaade',
'image_upload_success' => 'Pildifail üles laaditud',
'image_update_success' => 'Pildifaili andmed muudetud',
'image_delete_success' => 'Pildifail kustutatud',
'image_upload_remove' => 'Eemalda',
// Code Editor
'code_editor' => 'Muuda koodi',
'code_language' => 'Koodi keel',
'code_content' => 'Koodi sisu',
'code_session_history' => 'Sessiooni ajalugu',
'code_save' => 'Salvesta kood',
];

View File

@ -0,0 +1,325 @@
<?php
/**
* Text used for 'Entities' (Document Structure Elements) such as
* Books, Shelves, Chapters & Pages
*/
return [
// Shared
'recently_created' => 'Hiljuti lisatud',
'recently_created_pages' => 'Hiljuti lisatud lehed',
'recently_updated_pages' => 'Hiljuti muudetud lehed',
'recently_created_chapters' => 'Hiljuti lisatud peatükid',
'recently_created_books' => 'Hiljuti lisatud raamatud',
'recently_created_shelves' => 'Hiljuti lisatud riiulid',
'recently_update' => 'Hiljuti muudetud',
'recently_viewed' => 'Viimati vaadatud',
'recent_activity' => 'Hiljutised tegevused',
'create_now' => 'Create one now',
'revisions' => 'Redaktsioonid',
'meta_revision' => 'Redaktsioon #:revisionCount',
'meta_created' => 'Lisatud :timeLength',
'meta_created_name' => 'Lisatud :timeLength kasutaja :user poolt',
'meta_updated' => 'Muudetud :timeLength',
'meta_updated_name' => 'Muudetud :timeLength kasutaja :user poolt',
'meta_owned_name' => 'Owned by :user',
'entity_select' => 'Entity Select',
'images' => 'Pildid',
'my_recent_drafts' => 'Minu hiljutised mustandid',
'my_recently_viewed' => 'Minu viimati vaadatud',
'my_most_viewed_favourites' => 'Minu enim vaadatud lemmikud',
'my_favourites' => 'Minu lemmikud',
'no_pages_viewed' => 'Sa pole veel ühtegi lehte vaadanud',
'no_pages_recently_created' => 'Hiljuti pole ühtegi lehte lisatud',
'no_pages_recently_updated' => 'Hiljuti pole ühtegi lehte muudetud',
'export' => 'Ekspordi',
'export_html' => 'Contained Web File',
'export_pdf' => 'PDF fail',
'export_text' => 'Tekstifail',
'export_md' => 'Markdown fail',
// Permissions and restrictions
'permissions' => 'Õigused',
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
'permissions_enable' => 'Enable Custom Permissions',
'permissions_save' => 'Salvesta õigused',
'permissions_owner' => 'Omanik',
// Search
'search_results' => 'Otsingutulemused',
'search_total_results_found' => 'leitud :count vaste|leitud :count vastet',
'search_clear' => 'Tühjenda otsing',
'search_no_pages' => 'Otsing ei leidnud ühtegi lehte',
'search_for_term' => 'Search for :term',
'search_more' => 'Rohkem tulemusi',
'search_advanced' => 'Täpsem otsing',
'search_terms' => 'Otsinguterminid',
'search_content_type' => 'Sisu tüüp',
'search_exact_matches' => 'Täpsed vasted',
'search_tags' => 'Sildi otsing',
'search_options' => 'Valikud',
'search_viewed_by_me' => 'Minu vaadatud',
'search_not_viewed_by_me' => 'Minu vaatamata',
'search_permissions_set' => 'Õigused seatud',
'search_created_by_me' => 'Minu lisatud',
'search_updated_by_me' => 'Minu muudetud',
'search_owned_by_me' => 'Minu omad',
'search_date_options' => 'Kuupäeva valikud',
'search_updated_before' => 'Muudetud enne kui',
'search_updated_after' => 'Muudetud hiljem kui',
'search_created_before' => 'Lisatud enne kui',
'search_created_after' => 'Lisatud hiljem kui',
'search_set_date' => 'Vali kuupäev',
'search_update' => 'Värskenda otsingutulemusi',
// Shelves
'shelf' => 'Riiul',
'shelves' => 'Riiulid',
'x_shelves' => ':count riiul|:count riiulit',
'shelves_long' => 'Raamaturiiulid',
'shelves_empty' => 'Ühtegi riiulit pole lisatud',
'shelves_create' => 'Lisa uus riiul',
'shelves_popular' => 'Populaarsed riiulid',
'shelves_new' => 'Uued riiulid',
'shelves_new_action' => 'Uus riiul',
'shelves_popular_empty' => 'Siia tulevad kõige populaarsemad riiulid.',
'shelves_new_empty' => 'Siia tulevad hiljuti lisatud riiulid.',
'shelves_save' => 'Salvesta riiul',
'shelves_books' => 'Raamatud sellel riiulil',
'shelves_add_books' => 'Lisa sellele riiulile raamatuid',
'shelves_drag_books' => 'Lohista raamatuid siia, et neid sellele riiulile lisada',
'shelves_empty_contents' => 'Sellel riiulil ei ole ühtegi raamatut',
'shelves_edit_and_assign' => 'Muuda riiulit, et siia raamatuid lisada',
'shelves_edit_named' => 'Muuda riiulit :name',
'shelves_edit' => 'Muuda riiulit',
'shelves_delete' => 'Kustuta riiul',
'shelves_delete_named' => 'Kustuta riiul :name',
'shelves_delete_explain' => "See kustutab riiuli nimega ':name'. Raamatuid, mis on sellel riiulil, ei kustutata.",
'shelves_delete_confirmation' => 'Kas oled kindel, et soovid selle raamaturiiuli kustutada?',
'shelves_permissions' => 'Riiuli õigused',
'shelves_permissions_updated' => 'Riiuli õigused muudetud',
'shelves_permissions_active' => 'Riiuli õigused on aktiivsed',
'shelves_permissions_cascade_warning' => 'Raamaturiiuli õigused ei rakendu automaatselt sellel olevatele raamatutele, kuna raamat võib olla korraga mitmel riiulil. Alloleva valiku abil saab aga riiuli õigused kopeerida raamatutele.',
'shelves_copy_permissions_to_books' => 'Kopeeri õigused raamatutele',
'shelves_copy_permissions' => 'Kopeeri õigused',
'shelves_copy_permissions_explain' => 'See rakendab raamaturiiuli praegused õigused kõigile sellel olevatele raamatutele. Enne jätkamist veendu, et raamaturiiuli õiguste muudatused oleks salvestatud.',
'shelves_copy_permission_success' => 'Raamaturiiuli õigused kopeeritud :count raamatule',
// Books
'book' => 'Raamat',
'books' => 'Raamatud',
'x_books' => ':count raamat|:count raamatut',
'books_empty' => 'Ühtegi raamatut pole lisatud',
'books_popular' => 'Populaarsed raamatud',
'books_recent' => 'Hiljutised raamatud',
'books_new' => 'Uued raamatud',
'books_new_action' => 'Uus raamat',
'books_popular_empty' => 'Siia tulevad kõige populaarsemad raamatud.',
'books_new_empty' => 'Siia tulevad hiljuti lisatud raamatud.',
'books_create' => 'Lisa uus raamat',
'books_delete' => 'Kustuta raamat',
'books_delete_named' => 'Kustuta raamat :bookName',
'books_delete_explain' => 'See kustutab raamatu nimega \':bookName\'. Kõik lehed ja peatükid kustutatakse samuti.',
'books_delete_confirmation' => 'Kas oled kindel, et soovid selle raamatu kustutada?',
'books_edit' => 'Muuda raamatut',
'books_edit_named' => 'Muuda raamatut :bookName',
'books_form_book_name' => 'Raamatu pealkiri',
'books_save' => 'Salvesta raamat',
'books_permissions' => 'Raamatu õigused',
'books_permissions_updated' => 'Raamatu õigused muudetud',
'books_empty_contents' => 'Ühtegi lehte ega peatükki pole lisatud.',
'books_empty_create_page' => 'Lisa uus leht',
'books_empty_sort_current_book' => 'Sorteeri raamat',
'books_empty_add_chapter' => 'Lisa uus peatükk',
'books_permissions_active' => 'Raamatu õigused on aktiivsed',
'books_search_this' => 'Otsi sellest raamatust',
'books_navigation' => 'Raamatu sisukord',
'books_sort' => 'Sorteeri raamatu sisu',
'books_sort_named' => 'Sorteeri raamat :bookName',
'books_sort_name' => 'Sorteeri nime järgi',
'books_sort_created' => 'Sorteeri loomisaja järgi',
'books_sort_updated' => 'Sorteeri muutmisaja järgi',
'books_sort_chapters_first' => 'Peatükid eespool',
'books_sort_chapters_last' => 'Peatükid tagapool',
'books_sort_show_other' => 'Näita teisi raamatuid',
'books_sort_save' => 'Salvesta uus järjekord',
// Chapters
'chapter' => 'Peatükk',
'chapters' => 'Peatükid',
'x_chapters' => ':count peatükk|:count peatükki',
'chapters_popular' => 'Populaarsed peatükid',
'chapters_new' => 'Uus peatükk',
'chapters_create' => 'Lisa uus peatükk',
'chapters_delete' => 'Kustuta peatükk',
'chapters_delete_named' => 'Kustuta peatükk :chapterName',
'chapters_delete_explain' => 'See kustutab peatüki nimega \':chapterName\'. Kõik lehed selles peatükis kustutatakse samuti.',
'chapters_delete_confirm' => 'Kas oled kindel, et soovid selle peatüki kustutada?',
'chapters_edit' => 'Muuda peatükki',
'chapters_edit_named' => 'Muuda peatükki :chapterName',
'chapters_save' => 'Salvesta peatükk',
'chapters_move' => 'Liiguta peatükk',
'chapters_move_named' => 'Liiguta peatükk :chapterName',
'chapter_move_success' => 'Peatükk liigutatud raamatusse :bookName',
'chapters_permissions' => 'Peatüki õigused',
'chapters_empty' => 'Selles peatükis ei ole lehti.',
'chapters_permissions_active' => 'Peatüki õigused on aktiivsed',
'chapters_permissions_success' => 'Peatüki õigused muudetud',
'chapters_search_this' => 'Otsi sellest peatükist',
// Pages
'page' => 'Leht',
'pages' => 'Lehed',
'x_pages' => ':count leht|:count lehte',
'pages_popular' => 'Populaarsed lehed',
'pages_new' => 'Uus leht',
'pages_attachments' => 'Manused',
'pages_navigation' => 'Lehe sisukord',
'pages_delete' => 'Kustuta leht',
'pages_delete_named' => 'Kustuta leht :pageName',
'pages_delete_draft_named' => 'Kustuta mustand :pageName',
'pages_delete_draft' => 'Kustuta mustand',
'pages_delete_success' => 'Leht kustutatud',
'pages_delete_draft_success' => 'Mustand kustutatud',
'pages_delete_confirm' => 'Kas oled kindel, et soovid selle lehe kustutada?',
'pages_delete_draft_confirm' => 'Kas oled kindel, et soovid selle mustandi kustutada?',
'pages_editing_named' => 'Lehe :pageName muutmine',
'pages_edit_draft_options' => 'Mustandi valikud',
'pages_edit_save_draft' => 'Salvesta mustand',
'pages_edit_draft' => 'Muuda mustandit',
'pages_editing_draft' => 'Mustandi muutmine',
'pages_editing_page' => 'Lehe muutmine',
'pages_edit_draft_save_at' => 'Mustand salvestatud ',
'pages_edit_delete_draft' => 'Kustuta mustand',
'pages_edit_discard_draft' => 'Loobu mustandist',
'pages_edit_set_changelog' => 'Muudatuste logi',
'pages_edit_enter_changelog_desc' => 'Sisesta tehtud muudatuste lühikirjeldus',
'pages_edit_enter_changelog' => 'Salvesta muudatuste logi',
'pages_save' => 'Salvesta leht',
'pages_title' => 'Lehe pealkiri',
'pages_name' => 'Lehe nimetus',
'pages_md_editor' => 'Redaktor',
'pages_md_preview' => 'Eelvaade',
'pages_md_insert_image' => 'Lisa pilt',
'pages_md_insert_link' => 'Lisa viide',
'pages_md_insert_drawing' => 'Lisa joonis',
'pages_not_in_chapter' => 'Leht ei kuulu peatüki alla',
'pages_move' => 'Liiguta leht',
'pages_move_success' => 'Leht liigutatud ":parentName" alla',
'pages_copy' => 'Kopeeri leht',
'pages_copy_desination' => 'Copy Destination',
'pages_copy_success' => 'Leht on kopeeritud',
'pages_permissions' => 'Lehe õigused',
'pages_permissions_success' => 'Lehe õigused muudetud',
'pages_revision' => 'Redaktsioon',
'pages_revisions' => 'Lehe redaktsioonid',
'pages_revisions_named' => 'Lehe :pageName redaktsioonid',
'pages_revision_named' => 'Lehe :pageName redaktsioon',
'pages_revision_restored_from' => 'Taastatud redaktsioonist #:id; :summary',
'pages_revisions_created_by' => 'Autor',
'pages_revisions_date' => 'Redaktsiooni aeg',
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Redaktsioon #:id',
'pages_revisions_numbered_changes' => 'Redaktsiooni #:id muudatused',
'pages_revisions_changelog' => 'Muudatuste ajalugu',
'pages_revisions_changes' => 'Muudatused',
'pages_revisions_current' => 'Praegune versioon',
'pages_revisions_preview' => 'Eelvaade',
'pages_revisions_restore' => 'Taasta',
'pages_revisions_none' => 'Sellel lehel ei ole redaktsioone',
'pages_copy_link' => 'Kopeeri link',
'pages_edit_content_link' => 'Muuda sisu',
'pages_permissions_active' => 'Lehe õigused on aktiivsed',
'pages_initial_revision' => 'Esimene redaktsioon',
'pages_initial_name' => 'Uus leht',
'pages_editing_draft_notification' => 'Sa muudad mustandit, mis salvestati viimati :timeDiff.',
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
'pages_draft_page_changed_since_creation' => 'Seda lehte on pärast mustandi loomist muudetud. Soovitame mustandi ära visata või olla hoolikas, et mitte lehe muudatusi üle kirjutada.',
'pages_draft_edit_active' => [
'start_a' => ':count kasutajat on selle lehe muutmist alustanud',
'start_b' => ':userName alustas selle lehe muutmist',
'time_a' => 'lehe viimasest muutmisest alates',
'time_b' => 'viimase :minCount minuti jooksul',
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
'pages_specific' => 'Specific Page',
'pages_is_template' => 'Page Template',
// Editor Sidebar
'page_tags' => 'Lehe sildid',
'chapter_tags' => 'Peatüki sildid',
'book_tags' => 'Raamatu sildid',
'shelf_tags' => 'Riiuli sildid',
'tag' => 'Silt',
'tags' => 'Sildid',
'tag_name' => 'Sildi nimi',
'tag_value' => 'Sildi väärtus (valikuline)',
'tags_explain' => "Lisa silte, et sisu paremini organiseerida.\nVeel täpsemaks organiseerimiseks saad siltidele väärtuseid määrata.",
'tags_add' => 'Lisa veel üks silt',
'tags_remove' => 'Eemalda see silt',
'attachments' => 'Manused',
'attachments_explain' => 'Laadi üles faile või lisa linke, mida lehel kuvada. Need on nähtavad külgmenüüs.',
'attachments_explain_instant_save' => 'Muudatused salvestatakse koheselt.',
'attachments_items' => 'Lisatud objektid',
'attachments_upload' => 'Laadi fail üles',
'attachments_link' => 'Lisa link',
'attachments_set_link' => 'Set Link',
'attachments_delete' => 'Kas oled kindel, et soovid selle manuse kustutada?',
'attachments_dropzone' => 'Manuse lisamiseks lohista failid või klõpsa siin',
'attachments_no_files' => 'Üleslaaditud faile ei ole',
'attachments_explain_link' => 'Faili üleslaadimise asemel saad lingi lisada. See võib viidata teisele lehele või failile kuskil pilves.',
'attachments_link_name' => 'Lingi nimi',
'attachment_link' => 'Manuse link',
'attachments_link_url' => 'Link failile',
'attachments_link_url_hint' => 'Lehekülje või faili URL',
'attach' => 'Lisa',
'attachments_insert_link' => 'Add Attachment Link to Page',
'attachments_edit_file' => 'Muuda faili',
'attachments_edit_file_name' => 'Faili nimi',
'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
'attachments_order_updated' => 'Manuste järjekord muudetud',
'attachments_updated_success' => 'Manuse andmed muudetud',
'attachments_deleted' => 'Manus kustutatud',
'attachments_file_uploaded' => 'Fail on üles laaditud',
'attachments_file_updated' => 'Fail on muudetud',
'attachments_link_attached' => 'Link on lehele lisatud',
'templates' => 'Mallid',
'templates_set_as_template' => 'Leht on mall',
'templates_explain_set_as_template' => 'Sa saad määrata selle lehe malliks, nii et selle sisu saab kasutada uute lehtede lisamisel. Kui teistel kasutajatel on selle lehe vaatamiseks õigus, saavad ka nemad seda mallina kasutada.',
'templates_replace_content' => 'Asenda lehe sisu',
'templates_append_content' => 'Lisa lehe sisu järele',
'templates_prepend_content' => 'Lisa lehe sisu ette',
// Profile View
'profile_user_for_x' => 'User for :time',
'profile_created_content' => 'Lisatud sisu',
'profile_not_created_pages' => ':userName ei ole ühtegi lehte lisanud',
'profile_not_created_chapters' => ':userName ei ole ühtegi peatükki lisanud',
'profile_not_created_books' => ':userName ei ole ühtegi raamatut lisanud',
'profile_not_created_shelves' => ':userName ei ole ühtegi riiulit lisanud',
// Comments
'comment' => 'Kommentaar',
'comments' => 'Kommentaarid',
'comment_add' => 'Lisa kommentaar',
'comment_placeholder' => 'Jäta siia kommentaar',
'comment_count' => '{0} Kommentaare pole|{1} 1 kommentaar|[2,*] :count kommentaari',
'comment_save' => 'Salvesta kommentaar',
'comment_saving' => 'Kommentaari salvestamine...',
'comment_deleting' => 'Kommentaari kustutamine...',
'comment_new' => 'Uus kommentaar',
'comment_created' => 'kommenteeris :createDiff',
'comment_updated' => 'Muudetud :updateDiff :username poolt',
'comment_deleted_success' => 'Kommentaar kustutatud',
'comment_created_success' => 'Kommentaar lisatud',
'comment_updated_success' => 'Kommentaar muudetud',
'comment_delete_confirm' => 'Kas oled kindel, et soovid selle kommentaari kustutada?',
'comment_in_reply_to' => 'Vastus kommentaarile :commentId',
// Revision
'revision_delete_confirm' => 'Kas oled kindel, et soovid selle redaktsiooni kustutada?',
'revision_restore_confirm' => 'Kas oled kindel, et soovid selle redaktsiooni taastada? Lehe praegune sisu asendatakse.',
'revision_delete_success' => 'Redaktsioon kustutatud',
'revision_cannot_delete_latest' => 'Kõige viimast redaktsiooni ei saa kustutada.'
];

View File

@ -0,0 +1,109 @@
<?php
/**
* Text shown in error messaging.
*/
return [
// Permissions
'permission' => 'Sul puudub õigus selle lehe vaatamiseks.',
'permissionJson' => 'Sul puudub õigus selle tegevuse teostamiseks.',
// Auth
'error_user_exists_different_creds' => 'See e-posti aadress on juba seotud teise kasutajaga.',
'email_already_confirmed' => 'E-posti aadress on juba kinnitatud. Proovi sisse logida.',
'email_confirmation_invalid' => 'Kinnituslink ei ole kehtiv või on seda juba kasutatud. Proovi uuesti registreeruda.',
'email_confirmation_expired' => 'Kinnituslink on aegunud. Sulle saadeti aadressi kinnitamiseks uus e-kiri.',
'email_confirmation_awaiting' => 'Selle kasutajakonto e-posti aadress vajab kinnitamist',
'ldap_fail_anonymous' => 'LDAP anonüümne ligipääs ebaõnnestus',
'ldap_fail_authed' => 'LDAP ligipääs antud nime ja parooliga ebaõnnestus',
'ldap_extension_not_installed' => 'PHP LDAP laiendus ei ole paigaldatud',
'ldap_cannot_connect' => 'Ühendus LDAP serveriga ebaõnnestus',
'saml_already_logged_in' => 'Juba sisse logitud',
'saml_user_not_registered' => 'Kasutaja :name ei ole registreeritud ning automaatne registreerimine on keelatud',
'saml_no_email_address' => 'Selle kasutaja e-posti aadressi ei õnnestunud välisest autentimissüsteemist leida',
'saml_invalid_response_id' => 'Välisest autentimissüsteemist tulnud päringut ei algatatud sellest rakendusest. Seda viga võib põhjustada pärast sisselogimist tagasi liikumine.',
'saml_fail_authed' => 'Sisenemine :system kaudu ebaõnnestus, süsteem ei andnud volitust',
'oidc_already_logged_in' => 'Juba sisse logitud',
'oidc_user_not_registered' => 'Kasutaja :name ei ole registreeritud ning automaatne registreerimine on keelatud',
'oidc_no_email_address' => 'Selle kasutaja e-posti aadressi ei õnnestunud välisest autentimissüsteemist leida',
'oidc_fail_authed' => 'Sisenemine :system kaudu ebaõnnestus, süsteem ei andnud volitust',
'social_no_action_defined' => 'Tegevus on defineerimata',
'social_login_bad_response' => ":socialAccount kaudu sisselogimisel tekkis viga: \n:error",
'social_account_in_use' => 'See :socialAccount konto on juba kasutusel, proovi :socialAccount kaudu sisse logida.',
'social_account_email_in_use' => 'E-posti aadress :email on juba kasutusel. Kui sul on juba kasutajakonto, saad oma :socialAccount konto siduda profiili seadetes.',
'social_account_existing' => 'See :socialAccount konto on juba seotud su profiiliga.',
'social_account_already_used_existing' => 'See :socialAccount konto on juba seotud teise kasutajaga.',
'social_account_not_used' => 'See :socialAccount konto ei ole seotud ühegi kasutajaga. Seosta see oma profiili seadetes. ',
'social_account_register_instructions' => 'Kui sul pole veel kasutajakontot, saad selle registreerida :socialAccount kaudu.',
'social_driver_not_found' => 'Sotsiaalmeedia kontode draiverit ei leitud',
'social_driver_not_configured' => 'Sinu :socialAccount konto seaded ei ole korrektsed.',
'invite_token_expired' => 'Link on aegunud. Võid selle asemel proovida oma konto parooli lähtestada.',
// System
'path_not_writable' => 'Faili asukohaga :filePath ei õnnestunud üles laadida. Veendu, et serveril on kirjutusõigused.',
'cannot_get_image_from_url' => 'Ei suutnud laadida pilti aadressilt :url',
'cannot_create_thumbs' => 'Server ei saa piltide eelvaateid tekitada. Veendu, et PHP GD laiendus on paigaldatud.',
'server_upload_limit' => 'Server ei luba nii suurte failide üleslaadimist. Proovi väiksema failiga.',
'uploaded' => 'Server ei luba nii suurte failide üleslaadimist. Proovi väiksema failiga.',
'image_upload_error' => 'Pildi üleslaadimisel tekkis viga',
'image_upload_type_error' => 'Pildifaili tüüp ei ole korrektne',
'file_upload_timeout' => 'Faili üleslaadimine aegus.',
// Attachments
'attachment_not_found' => 'Manust ei leitud',
// Pages
'page_draft_autosave_fail' => 'Mustandi salvestamine ebaõnnestus. Kontrolli oma internetiühendust',
'page_custom_home_deletion' => 'Ei saa kustutada lehte, mis on määratud avaleheks',
// Entities
'entity_not_found' => 'Objekti ei leitud',
'bookshelf_not_found' => 'Riiulit ei leitud',
'book_not_found' => 'Raamatut ei leitud',
'page_not_found' => 'Lehte ei leitud',
'chapter_not_found' => 'Peatükki ei leitud',
'selected_book_not_found' => 'Valitud raamatut ei leitud',
'selected_book_chapter_not_found' => 'Valitud raamatut või peatükki ei leitud',
'guests_cannot_save_drafts' => 'Külalised ei saa mustandeid salvestada',
// Users
'users_cannot_delete_only_admin' => 'Ainsat administraatorit ei saa kustutada',
'users_cannot_delete_guest' => 'Külaliskasutajat ei saa kustutada',
// Roles
'role_cannot_be_edited' => 'Seda rolli ei saa muuta',
'role_system_cannot_be_deleted' => 'See roll on süsteemne ja seda ei saa kustutada',
'role_registration_default_cannot_delete' => 'Seda rolli ei saa kustutada, kuna see on seatud uute kasutajate vaikimisi rolliks',
'role_cannot_remove_only_admin' => 'See kasutaja on ainus, kellel on administraatori roll. Enne kustutamist lisa administraatori roll mõnele teisele kasutajale.',
// Comments
'comment_list' => 'Kommentaaride pärimisel tekkis viga.',
'cannot_add_comment_to_draft' => 'Mustandile ei saa kommentaare lisada.',
'comment_add' => 'Kommentaari lisamisel / muutmisel tekkis viga.',
'comment_delete' => 'Kommentaari kustutamisel tekkis viga.',
'empty_comment' => 'Tühja kommentaari ei saa lisada.',
// Error pages
'404_page_not_found' => 'Lehekülge ei leitud',
'sorry_page_not_found' => 'Vabandust, soovitud lehekülge ei leitud.',
'sorry_page_not_found_permission_warning' => 'Kui see lehekülg peaks kindlalt olemas olema, ei pruugi sul olla õigust selle vaatamiseks.',
'image_not_found' => 'Pildifaili ei leitud',
'image_not_found_subtitle' => 'Vabandust, soovitud pildifaili ei leitud.',
'image_not_found_details' => 'Kui sa eeldasid, et see pildifail on olemas, võib see olla kustutatud.',
'return_home' => 'Tagasi avalehele',
'error_occurred' => 'Tekkis viga',
'app_down' => ':appName on hetkel maas',
'back_soon' => 'See on varsti tagasi.',
// API errors
'api_no_authorization_found' => 'Päringust ei leitud volitustunnust',
'api_bad_authorization_format' => 'Päringust leiti volitustunnus, aga see ei olnud korrektses formaadis',
'api_user_token_not_found' => 'Volitustunnusele vastavat API tunnust ei leitud',
'api_incorrect_token_secret' => 'API tunnusele lisatud salajane võti ei ole korrektne',
'api_user_no_api_permission' => 'Selle API tunnuse omanikul ei ole õigust API päringuid teha',
'api_user_token_expired' => 'Volitustunnus on aegunud',
// Settings & Maintenance
'maintenance_test_email_failure' => 'Test e-kirja saatmisel tekkis viga:',
];

View File

@ -0,0 +1,12 @@
<?php
/**
* Pagination Language Lines
* The following language lines are used by the paginator library to build
* the simple pagination links.
*/
return [
'previous' => '&laquo; Eelmine',
'next' => 'Järgmine &raquo;',
];

View File

@ -0,0 +1,15 @@
<?php
/**
* Password Reminder Language Lines
* The following language lines are the default lines which match reasons
* that are given by the password broker for a password update attempt has failed.
*/
return [
'password' => 'Paroolides peab olema vähemalt kaheksa tähemärki ja nad peavad omavahel ühtima.',
'user' => "Sellise e-posti aadressiga kasutajat ei leitud.",
'token' => 'Parooli lähtestamise link ei kehti selle e-posti aadressiga.',
'sent' => 'Parooli lähtestamise link saadeti e-postiga!',
'reset' => 'Parool on lähtestatud!',
];

View File

@ -0,0 +1,278 @@
<?php
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
return [
// Common Messages
'settings' => 'Seaded',
'settings_save' => 'Salvesta seaded',
'settings_save_success' => 'Seaded salvestatud',
// App Settings
'app_customization' => 'Kohandamine',
'app_features_security' => 'Funktsioonid ja turvalisus',
'app_name' => 'Rakenduse nimi',
'app_name_desc' => 'Seda nime näidatakse päises ja kõigis süsteemsetes e-kirjades.',
'app_name_header' => 'Näita nime päises',
'app_public_access' => 'Avalik ligipääs',
'app_public_access_desc' => 'Selle sisselülitamine võimaldab külalistel ilma sisselogimata ligipääsu su BookStack\'i sisule.',
'app_public_access_desc_guest' => 'Sisselogimata kasutajate ligipääsu saab seadistada "Külaline" kasutaja kaudu.',
'app_public_access_toggle' => 'Luba avalik ligipääs',
'app_public_viewing' => 'Luba avalik ligipääs?',
'app_secure_images' => 'Turvalisem piltide üleslaadimine',
'app_secure_images_toggle' => 'Lülita sisse turvalisem piltide üleslaadimine',
'app_secure_images_desc' => 'Jõudluse kaalutlustel on kõik pildifailid avalikult kättesaadavad. See valik lisab pildifailide URL-ide ette juhugenereeritud, raskesti arvatava stringi. Ligipääsu piiramiseks veendu, et kataloogide indekseerimine ei oleks lubatud.',
'app_editor' => 'Redaktor',
'app_editor_desc' => 'Vali, millist redaktorit kasutajad lehtede muutmiseks kasutavad.',
'app_custom_html' => 'Kohandatud HTML päise sisu',
'app_custom_html_desc' => 'Siia lisatud sisu lisatakse iga lehe <head> sektsiooni lõppu. See võimaldab stiile üle laadida või lisada analüütika koodi.',
'app_custom_html_disabled_notice' => 'Kohandatud HTML päise sisu on sellel lehel välja lülitatud, et probleemseid muudatusi saaks tagasi võtta.',
'app_logo' => 'Rakenduse logo',
'app_logo_desc' => 'See pildifail peaks olema 43 pikslit kõrge. <br>Suuremad pildifailid tehakse väiksemaks.',
'app_primary_color' => 'Rakenduse põhivärv',
'app_primary_color_desc' => 'Määrab rakenduse primaarse värvi, sh. päise, nuppude ja linkide jaoks.',
'app_homepage' => 'Rakenduse avaleht',
'app_homepage_desc' => 'Vali leht, mida näidata avalehel vaikimisi vaate asemel. Valitud lehele ei rakendata ligipääsuõiguseid.',
'app_homepage_select' => 'Vali leht',
'app_footer_links' => 'Lingid jaluses',
'app_footer_links_desc' => 'Lisa rakenduse jalusesse linke. Neid näidatakse enamike lehtede jaluses, kaasa arvatud need, mis ei vaja sisselogimist. Võid kasutada märgendit "trans::<key>", et kasutada süsteemseid tõlkeid. Näiteks "trans::common.privacy_policy" tekitab tõlgitud teksti "Privaatsus" ning "trans::common.terms_of_service" tekitab tõlgitud teksti "Kasutustingimused".',
'app_footer_links_label' => 'Lingi tekst',
'app_footer_links_url' => 'Lingi URL',
'app_footer_links_add' => 'Lisa link',
'app_disable_comments' => 'Keela kommentaarid',
'app_disable_comments_toggle' => 'Keela kommentaarid',
'app_disable_comments_desc' => 'Keelab kommentaarid kogu rakenduses. <br>Olemasolevaid kommentaare ei näidata.',
// Color settings
'content_colors' => 'Sisuelementide värvid',
'content_colors_desc' => 'Määrab värvid erinevatele sisuelementidele. Loetavuse huvides on soovituslik valida värvid, mille heledus on sarnane vaikimisi värvidele.',
'bookshelf_color' => 'Riiuli värv',
'book_color' => 'Raamatu värv',
'chapter_color' => 'Peatüki värv',
'page_color' => 'Lehe värv',
'page_draft_color' => 'Mustandi värv',
// Registration Settings
'reg_settings' => 'Registreerumine',
'reg_enable' => 'Luba registreerumine',
'reg_enable_toggle' => 'Luba registreerumine',
'reg_enable_desc' => 'Kui registreerumine on lubatud, saavad kasutajad ise endale rakenduse konto tekitada, ning neile antakse vaikimisi roll.',
'reg_default_role' => 'Vaikimisi roll uutele kasutajatele',
'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
'reg_email_confirmation' => 'E-posti aadressi kinnitus',
'reg_email_confirmation_toggle' => 'Nõua e-posti aadressi kinnitamist',
'reg_confirm_email_desc' => 'Kui domeeni piirang on kasutusel, siis on e-posti aadressi kinnitamine nõutud ja seda seadet ignoreeritakse.',
'reg_confirm_restrict_domain' => 'Domeeni piirang',
'reg_confirm_restrict_domain_desc' => 'Sisesta komaga eraldatud nimekiri e-posti domeenidest, millega soovitud registreerumist piirata. Kasutajale saadetakse aadressi kinnitamiseks e-kiri, enne kui neil lubatakse rakendust kasutada.<br>Pane tähele, et kasutajad saavad pärast edukat registreerumist oma e-posti aadressi muuta.',
'reg_confirm_restrict_domain_placeholder' => 'Piirangut ei ole',
// Maintenance settings
'maint' => 'Hooldus',
'maint_image_cleanup' => 'Pildifailide koristus',
'maint_image_cleanup_desc' => "Kontrollib lehtede ja redaktsioonide sisu, et leida pilte ja jooniseid, mis enam kasutusel ei ole. Enne selle käivitamist tee andmebaasist ja pildifailidest täielik varukoopia.",
'maint_delete_images_only_in_revisions' => 'Kustuta ka pildifailid, mis on kasutusel ainult vanades redaktsioonides',
'maint_image_cleanup_run' => 'Käivita koristus',
'maint_image_cleanup_warning' => 'Leiti :count potentsiaalselt kasutamata pildifaili. Kas oled kindel, et soovid need kustutada?',
'maint_image_cleanup_success' => 'Leiti ja kustutati :count potentsiaalselt kasutamata pildifaili!',
'maint_image_cleanup_nothing_found' => 'Kasutamata pildifaile ei leitud, pole midagi kustutada!',
'maint_send_test_email' => 'Saada testimiseks e-kiri',
'maint_send_test_email_desc' => 'See saadab testimiseks e-kirja su profiilis märgitud aadressile.',
'maint_send_test_email_run' => 'Saada test e-kiri',
'maint_send_test_email_success' => 'E-kiri saadetud aadressile :address',
'maint_send_test_email_mail_subject' => 'Test Email',
'maint_send_test_email_mail_greeting' => 'E-kirjade saatmine tundub toimivat!',
'maint_send_test_email_mail_text' => 'Hea töö! Kui sa selle e-kirja kätte said, on su e-posti seaded õigesti määratud.',
'maint_recycle_bin_desc' => 'Kustutatud riiulid, raamatud, peatükid ja lehed saadetakse prügikasti, et neid saaks taastada või lõplikult kustutada. Vanemad objektid võidakse teatud aja järel automaatselt prügikastist kustutada.',
'maint_recycle_bin_open' => 'Ava prügikast',
// Recycle Bin
'recycle_bin' => 'Prügikast',
'recycle_bin_desc' => 'Siin saad taastada kustutatud objekte, või neid süsteemist lõplikult eemaldada. Nimekiri on filtreerimata, mitte nagu mujal tegevusloendites, kus rakenduvad õigused.',
'recycle_bin_deleted_item' => 'Kustutatud objekt',
'recycle_bin_deleted_parent' => 'Ülemobjekt',
'recycle_bin_deleted_by' => 'Kustutaja',
'recycle_bin_deleted_at' => 'Kustutamise aeg',
'recycle_bin_permanently_delete' => 'Kustuta lõplikult',
'recycle_bin_restore' => 'Taasta',
'recycle_bin_contents_empty' => 'Prügikast on hetkel tühi',
'recycle_bin_empty' => 'Tühjenda prügikast',
'recycle_bin_empty_confirm' => 'See kustutab lõplikult kõik objektid prügikastis, kaasa arvatud nende sisu. Kas oled kindel, et soovid prügikasti tühjendada?',
'recycle_bin_destroy_confirm' => 'See kustutab lõplikult valitud objekti koos loetletud alamobjektidega, ja seda sisu ei ole enam võimalik taastada. Kas oled kindel, et soovid selle objekti kustutada?',
'recycle_bin_destroy_list' => 'Kustutatavad objektid',
'recycle_bin_restore_list' => 'Taastatavad objektid',
'recycle_bin_restore_confirm' => 'See taastab valitud objekti koos kõigi alamobjektidega nende algsesse asukohta. Kui see asukoht on ka vahepeal kustutatud ja on nüüd prügikastis, tuleb ka see taastada.',
'recycle_bin_restore_deleted_parent' => 'Selle objekti ülemobjekt on ka kustutatud. Taastada ei saa enne, kui ülemobjekt on taastatud.',
'recycle_bin_restore_parent' => 'Taasta ülemobjekt',
'recycle_bin_destroy_notification' => 'Prügikastist kustutati :count objekti.',
'recycle_bin_restore_notification' => 'Prügikastist taastati :count objekti.',
// Audit Log
'audit' => 'Auditilogi',
'audit_desc' => 'Auditilogi kuvab nimekirja tegevustest, mida süsteem jälgib. See nimekiri on filtreerimata, erinevalt muudest loenditest süsteemis, kus rakenduvad õigused.',
'audit_event_filter' => 'Sündmuse filter',
'audit_event_filter_no_filter' => 'Ilma filtrita',
'audit_deleted_item' => 'Kustutatud objekt',
'audit_deleted_item_name' => 'Nimi: :name',
'audit_table_user' => 'Kasutaja',
'audit_table_event' => 'Sündmus',
'audit_table_related' => 'Seotud objekt või detail',
'audit_table_ip' => 'IP-aadress',
'audit_table_date' => 'Tegevuse aeg',
'audit_date_from' => 'Kuupäev alates',
'audit_date_to' => 'Kuupäev kuni',
// Role Settings
'roles' => 'Rollid',
'role_user_roles' => 'Kasutajate rollid',
'role_create' => 'Lisa uus roll',
'role_create_success' => 'Roll on lisatud',
'role_delete' => 'Kustuta roll',
'role_delete_confirm' => 'See kustutab rolli nimega \':roleName\'.',
'role_delete_users_assigned' => 'Selle rolliga on seotud :userCount kasutajat. Kui soovid neile selle asemel uue rolli määrata, siis vali see allpool.',
'role_delete_no_migration' => "Ära määra uut rolli",
'role_delete_sure' => 'Kas oled kindel, et soovid selle rolli kustutada?',
'role_delete_success' => 'Roll on kustutatud',
'role_edit' => 'Muuda rolli',
'role_details' => 'Rolli detailid',
'role_name' => 'Rolli nimi',
'role_desc' => 'Rolli lühike kirjeldus',
'role_mfa_enforced' => 'Vajab mitmeastmelist autentimist',
'role_external_auth_id' => 'Välise autentimise ID-d',
'role_system' => 'Süsteemsed õigused',
'role_manage_users' => 'Kasutajate haldamine',
'role_manage_roles' => 'Rollide ja õiguste haldamine',
'role_manage_entity_permissions' => 'Kõigi raamatute, peatükkide ja lehtede õiguste haldamine',
'role_manage_own_entity_permissions' => 'Oma raamatute, peatükkide ja lehtede õiguste haldamine',
'role_manage_page_templates' => 'Mallide haldamine',
'role_access_api' => 'Süsteemi API ligipääs',
'role_manage_settings' => 'Rakenduse seadete haldamine',
'role_export_content' => 'Sisu eksport',
'role_asset' => 'Sisu õigused',
'roles_system_warning' => 'Pane tähele, et ülalolevad kolm õigust võimaldavad kasutajal enda või teiste kasutajate õiguseid muuta. Määra nende õigustega roll ainult usaldusväärsetele kasutajatele.',
'role_asset_desc' => 'Need load kontrollivad vaikimisi ligipääsu süsteemis olevale sisule. Raamatute, peatükkide ja lehtede õigused rakenduvad esmajärjekorras.',
'role_asset_admins' => 'Administraatoritel on automaatselt ligipääs kogu sisule, aga need valikud võivad peida või näidata kasutajaliidese elemente.',
'role_all' => 'Kõik',
'role_own' => 'Enda omad',
'role_controlled_by_asset' => 'Õigused määratud seotud objekti kaudu',
'role_save' => 'Salvesta roll',
'role_update_success' => 'Roll on muudetud',
'role_users' => 'Selle rolliga kasutajad',
'role_users_none' => 'Seda rolli ei ole hetkel ühelgi kasutajal',
// Users
'users' => 'Kasutajad',
'user_profile' => 'Kasutajaprofiil',
'users_add_new' => 'Lisa uus kasutaja',
'users_search' => 'Otsi kasutajaid',
'users_latest_activity' => 'Viimane tegevus',
'users_details' => 'Kasutaja andmed',
'users_details_desc' => 'Määra kasutajale nimi ja e-posti aadress. E-posti aadressi kasutatakse rakendusse sisse logimiseks.',
'users_details_desc_no_email' => 'Määra kasutajale nimi, mille järgi teised ta ära tunnevad.',
'users_role' => 'Kasutaja rollid',
'users_role_desc' => 'Vali, millised rollid sellel kasutajal on. Kui talle on valitud mitu rolli, siis nende õigused kombineeritakse ja kasutaja saab kõigi rollide õigused.',
'users_password' => 'Kasutaja parool',
'users_password_desc' => 'Määra kasutajale parool, millega rakendusse sisse logida. See peab olema vähemalt 6 tähemärki.',
'users_send_invite_text' => 'Sa võid kasutajale saata e-postiga kutse, mis võimaldab neil ise parooli seada. Vastasel juhul määra parool ise.',
'users_send_invite_option' => 'Saada e-postiga kutse',
'users_external_auth_id' => 'Välise autentimise ID',
'users_external_auth_id_desc' => 'Selle ID abil identifitseeritakse kasutajat välise autentimissüsteemiga suhtlemisel.',
'users_password_warning' => 'Täida allolevad väljad ainult siis, kui soovid oma parooli muuta.',
'users_system_public' => 'See kasutaja tähistab kõiki külalisi, kes su rakendust vaatavad. Selle kontoga ei saa sisse logida, see määratakse automaatselt.',
'users_delete' => 'Kustuta kasutaja',
'users_delete_named' => 'Kustuta kasutaja :userName',
'users_delete_warning' => 'See kustutab kasutaja nimega \':userName\' süsteemist täielikult.',
'users_delete_confirm' => 'Kas oled kindel, et soovid selle kasutaja kustutada?',
'users_migrate_ownership' => 'Teisalda omandus',
'users_migrate_ownership_desc' => 'Vali siin kasutaja, kui soovid talle üle viia kõik selle kasutaja objektid.',
'users_none_selected' => 'Kasutaja valimata',
'users_delete_success' => 'Kasutaja on kustutatud',
'users_edit' => 'Muuda kasutajat',
'users_edit_profile' => 'Muuda profiili',
'users_edit_success' => 'Kasutaja on muudetud',
'users_avatar' => 'Kasutaja profiilipilt',
'users_avatar_desc' => 'Vali sellele kasutajale profiilipilt. See peaks olema umbes 256x256 pikslit.',
'users_preferred_language' => 'Eelistatud keel',
'users_preferred_language_desc' => 'See valik muudab rakenduse kasutajaliidese keelt. Kasutajate loodud sisu see ei mõjuta.',
'users_social_accounts' => 'Sotsiaalmeedia kontod',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
'users_social_connect' => 'Lisa konto',
'users_social_disconnect' => 'Eemalda konto',
'users_social_connected' => ':socialAccount konto lisati su profiilile.',
'users_social_disconnected' => ':socialAccount konto eemaldati su profiililt.',
'users_api_tokens' => 'API tunnused',
'users_api_tokens_none' => 'Sellel kasutajal pole API tunnuseid',
'users_api_tokens_create' => 'Lisa tunnus',
'users_api_tokens_expires' => 'Aegub',
'users_api_tokens_docs' => 'API dokumentatsioon',
'users_mfa' => 'Mitmeastmeline autentimine',
'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
// API Tokens
'user_api_token_create' => 'Lisa API tunnus',
'user_api_token_name' => 'Nimi',
'user_api_token_name_desc' => 'Anna oma tunnusele inimloetav nimi, et selle eesmärk paremini meeles püsiks.',
'user_api_token_expiry' => 'Kehtiv kuni',
'user_api_token_expiry_desc' => 'Määra kuupäev, millal see tunnus aegub. Pärast seda kuupäeva ei saa selle tunnusega enam päringuid teha. Välja tühjaks jätmine määrab aegumiskuupäeva 100 aastat tulevikku.',
'user_api_token_create_secret_message' => 'Kohe pärast selle tunnuse loomist genereeritakse ja kuvatakse tunnuse ID ja salajane võti. Võtit kuvatakse ainult ühe korra, seega kopeeri selle väärtus enne jätkamist turvalisse kohta.',
'user_api_token_create_success' => 'API tunnus on lisatud',
'user_api_token_update_success' => 'API tunnus on muudetud',
'user_api_token' => 'API tunnus',
'user_api_token_id' => 'Tunnuse ID',
'user_api_token_id_desc' => 'See on API tunnuse süsteemne mittemuudetav identifikaator, mis tuleb API päringutele kaasa panna.',
'user_api_token_secret' => 'Tunnuse võti',
'user_api_token_secret_desc' => 'See on API tunnuse salajane võti, mis tuleb API päringutele kaasa panna. Seda kuvatakse ainult ühe korra, seega kopeeri see turvalisse kohta.',
'user_api_token_created' => 'Tunnus lisatud :timeAgo',
'user_api_token_updated' => 'Tunnus muudetud :timeAgo',
'user_api_token_delete' => 'Kustuta tunnus',
'user_api_token_delete_warning' => 'See kustutab API tunnuse nimega \':tokenName\' süsteemist.',
'user_api_token_delete_confirm' => 'Kas oled kindel, et soovid selle API tunnuse kustutada?',
'user_api_token_delete_success' => 'API tunnus on kustutatud',
//! 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',
'et' => 'Eesti Keel',
'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' => '繁體中文',
]
//!////////////////////////////////
];

View File

@ -0,0 +1,116 @@
<?php
/**
* Validation Lines
* The following language lines contain the default error messages used by
* the validator class. Some of these rules have multiple versions such
* as the size rules. Feel free to tweak each of these messages here.
*/
return [
// Standard laravel validation lines
'accepted' => 'The :attribute must be accepted.',
'active_url' => ':attribute ei ole kehtiv URL.',
'after' => ':attribute peab olema kuupäev pärast :date.',
'alpha' => ':attribute võib sisaldada ainult tähti.',
'alpha_dash' => ':attribute võib sisaldada ainult tähti, numbreid, sidekriipse ja alakriipse.',
'alpha_num' => ':attribute võib sisaldada ainult tähti ja numbreid.',
'array' => ':attribute peab olema massiiv.',
'backup_codes' => 'The provided code is not valid or has already been used.',
'before' => ':attribute peab olema kuupäev enne :date.',
'between' => [
'numeric' => ':attribute peab jääma vahemikku :min ja :max.',
'file' => ':attribute peab olema :min ja :max kilobaidi vahel.',
'string' => ':attribute peab olema :min ja :max tähemärgi vahel.',
'array' => ':attribute peab olema :min ja :max elemendi vahel.',
],
'boolean' => ':attribute peab olema tõene või väär.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => ':attribute ei ole kehtiv kuupäev.',
'date_format' => ':attribute ei ühti formaadiga :format.',
'different' => ':attribute ja :other peavad olema erinevad.',
'digits' => ':attribute peab olema :digits-kohaline arv.',
'digits_between' => ':attribute peab olema :min ja :max numbri vahel.',
'email' => ':attribute peab olema kehtiv e-posti aadress.',
'ends_with' => ':attribute lõpus peab olema üks järgmistest väärtustest: :values',
'filled' => ':attribute väli on kohustuslik.',
'gt' => [
'numeric' => ':attribute peab olema suurem kui :value.',
'file' => ':attribute peab olema suurem kui :value kilobaiti.',
'string' => ':attribute peab sisaldama rohkem kui :value tähemärki.',
'array' => ':attribute peab sisaldama rohkem kui :value elementi.',
],
'gte' => [
'numeric' => ':attribute peab olema suurem kui või võrdne :value.',
'file' => ':attribute peab olema :value kilobaiti või rohkem.',
'string' => ':attribute peab sisaldama :value või rohkem tähemärki.',
'array' => ':attribute peab sisaldama :value või rohkem elementi.',
],
'exists' => 'Valitud :attribute on vigane.',
'image' => ':attribute peab olema pildifail.',
'image_extension' => 'The :attribute must have a valid & supported image extension.',
'in' => 'Valitud :attribute on vigane.',
'integer' => ':attribute peab olema täisarv.',
'ip' => ':attribute peab olema kehtiv IP-aadress.',
'ipv4' => ':attribute peab olema kehtiv IPv4 aadress.',
'ipv6' => ':attribute peab olema kehtiv IPv6 aadress.',
'json' => ':attribute peab olema kehtiv JSON-vormingus tekst.',
'lt' => [
'numeric' => ':attribute peab olema väiksem kui :value.',
'file' => ':attribute peab olema väiksem kui :value kilobaiti.',
'string' => ':attribute peab sisaldama vähem kui :value tähemärki.',
'array' => ':attribute peab sisaldama vähem kui :value elementi.',
],
'lte' => [
'numeric' => ':attribute peab olema :value või vähem.',
'file' => ':attribute peab olema :value kilobaiti või vähem.',
'string' => ':attribute peab sisaldama :value või vähem tähemärki.',
'array' => ':attribute ei tohi sisaldada rohkem kui :value elementi.',
],
'max' => [
'numeric' => ':attribute ei tohi olla suurem kui :max.',
'file' => ':attribute ei tohi olla suurem kui :max kilobaiti.',
'string' => ':attribute ei tohi sisaldada rohkem kui :max tähemärki.',
'array' => ':attribute ei tohi sisaldada rohkem kui :max elementi.',
],
'mimes' => ':attribute peab olema seda tüüpi fail: :values.',
'min' => [
'numeric' => ':attribute peab olema vähemalt :min.',
'file' => ':attribute peab olema vähemalt :min kilobaiti.',
'string' => ':attribute peab sisaldama vähemalt :min tähemärki.',
'array' => ':attribute peab sisaldama vähemalt :min elementi.',
],
'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' => ':attribute on kohustuslik.',
'required_if' => ':attribute on kohustuslik, kui :other on :value.',
'required_with' => ':attribute on kohustuslik, kui :values on olemas.',
'required_with_all' => ':attribute on kohustuslik, kui :values on olemas.',
'required_without' => ':attribute on kohustuslik, kui :values ei ole olemas.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => ':attribute ja :other peavad klappima.',
'safe_url' => 'Link ei pruugi olla turvaline.',
'size' => [
'numeric' => ':attribute peab olema :size.',
'file' => ':attribute peab olema :size kilobaiti.',
'string' => ':attribute peab sisaldama :size tähemärki.',
'array' => ':attribute peab sisaldama :size elemente.',
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'totp' => 'Kood ei ole korrektne või on aegunud.',
'unique' => ':attribute on juba võetud.',
'url' => ':attribute on vigases formaadis.',
'uploaded' => 'Faili üleslaadimine ebaõnnestus. Server ei pruugi sellise suurusega faile vastu võtta.',
// Custom validation lines
'custom' => [
'password-confirm' => [
'required_with' => 'Parooli kinnitus on nõutud',
],
],
// Custom validation attributes
'attributes' => [],
];

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'آدرس داده ای برای این کاربر در داده های ارائه شده توسط سیستم احراز هویت خارجی یافت نشد',
'saml_invalid_response_id' => 'درخواست از سیستم احراز هویت خارجی توسط فرایندی که توسط این نرم افزار آغاز شده است شناخته نمی شود. بازگشت به سیستم پس از ورود به سیستم می تواند باعث این مسئله شود.',
'saml_fail_authed' => 'ورود به سیستم :system انجام نشد، سیستم مجوز موفقیت آمیز ارائه نکرد',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'عملی تعریف نشده است',
'social_login_bad_response' => "خطای دریافت شده در هنگام ورود به سیستم:\n:error",
'social_account_in_use' => 'این حساب :socialAccount از قبل در حال استفاده است، سعی کنید از طریق گزینه :socialAccount وارد سیستم شوید.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Impossible de trouver une adresse e-mail, pour cet utilisateur, dans les données fournies par le système d\'authentification externe',
'saml_invalid_response_id' => 'La requête du système d\'authentification externe n\'est pas reconnue par un processus démarré par cette application. Naviguer après une connexion peut causer ce problème.',
'saml_fail_authed' => 'Connexion avec :system échouée, le système n\'a pas fourni l\'autorisation réussie',
'oidc_already_logged_in' => 'Déjà connecté',
'oidc_user_not_registered' => 'L\'utilisateur :name n\'est pas enregistré et l\'enregistrement automatique est désactivé',
'oidc_no_email_address' => 'Impossible de trouver une adresse e-mail pour cet utilisateur, dans les données fournies par le système d\'authentification externe',
'oidc_fail_authed' => 'La connexion en utilisant :system a échoué, le système n\'a pas fourni d\'autorisation avec succès',
'social_no_action_defined' => 'Pas d\'action définie',
'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'Hébreu',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'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',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'לא הוגדרה פעולה',
'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
'social_account_in_use' => 'החשבון :socialAccount כבר בשימוש. אנא נסה להתחבר באמצעות אפשרות :socialAccount',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Nismo pronašli email adresu za ovog korisnika u vanjskim sustavima',
'saml_invalid_response_id' => 'Sustav za autentifikaciju nije prepoznat. Ovaj problem možda je nastao zbog vraćanja nakon prijave.',
'saml_fail_authed' => 'Prijava pomoću :system nije uspjela zbog neuspješne autorizacije',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Nije definirana nijedna radnja',
'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
'social_account_in_use' => 'Ovaj :socialAccount račun se već koristi. Pokušajte se prijaviti pomoću :socialAccount računa.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

View File

@ -23,6 +23,10 @@ return [
'saml_no_email_address' => 'Ehhez a felhasználóhoz nem található email cím a külső hitelesítő rendszer által átadott adatokban',
'saml_invalid_response_id' => 'A külső hitelesítő rendszerből érkező kérést nem ismerte fel az alkalmazás által indított folyamat. Bejelentkezés után az előző oldalra történő visszalépés okozhatja ezt a hibát.',
'saml_fail_authed' => 'Bejelentkezés :system használatával sikertelen, a rendszer nem biztosított sikeres hitelesítést',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'Nincs művelet meghatározva',
'social_login_bad_response' => "Hiba történt :socialAccount bejelentkezés közben:\n:error",
'social_account_in_use' => ':socialAccount fiók már használatban van. :socialAccount opción keresztül érdemes megpróbálni a bejelentkezést.',

View File

@ -248,6 +248,7 @@ return [
'de_informal' => 'Deutsch (Du)',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'et' => 'Eesti Keel',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',

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