mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-03-01 03:09:42 +03:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009212ab80 | ||
|
|
ba9cb591c8 | ||
|
|
632cb71af4 | ||
|
|
74ab99ec41 | ||
|
|
aa9dafec85 | ||
|
|
73a37b3cd9 | ||
|
|
e43f679e62 | ||
|
|
57fc1ba38f | ||
|
|
e765e61854 | ||
|
|
867cbe15ea | ||
|
|
b22dd3cb88 | ||
|
|
d00ac2f34e | ||
|
|
bd4dc6d463 | ||
|
|
e6c8ecba9c | ||
|
|
9490457d04 | ||
|
|
3e97fdf827 | ||
|
|
3b3eb0f44f | ||
|
|
b4fa82e329 | ||
|
|
42703dd859 | ||
|
|
2c21850da7 | ||
|
|
709533c1fb | ||
|
|
d91180a909 | ||
|
|
bc2913a5cb | ||
|
|
cd7788f2e9 | ||
|
|
f63d7f60aa | ||
|
|
197caddf96 | ||
|
|
096ed722dd | ||
|
|
024924eef3 | ||
|
|
1bf59f434b | ||
|
|
c6e196989e | ||
|
|
cb30c258df | ||
|
|
cdaad2f40e | ||
|
|
4ddbc9556b | ||
|
|
9a5adc026a | ||
|
|
37db51a627 | ||
|
|
f8c16494fd | ||
|
|
0d740ca681 | ||
|
|
876bc10d4d | ||
|
|
754403a29e |
@@ -134,7 +134,7 @@ STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
|
|||||||
STORAGE_URL=false
|
STORAGE_URL=false
|
||||||
|
|
||||||
# Authentication method to use
|
# Authentication method to use
|
||||||
# Can be 'standard', 'ldap' or 'saml2'
|
# Can be 'standard', 'ldap', 'saml2' or 'oidc'
|
||||||
AUTH_METHOD=standard
|
AUTH_METHOD=standard
|
||||||
|
|
||||||
# Social authentication configuration
|
# Social authentication configuration
|
||||||
@@ -242,6 +242,7 @@ SAML2_GROUP_ATTRIBUTE=group
|
|||||||
SAML2_REMOVE_FROM_GROUPS=false
|
SAML2_REMOVE_FROM_GROUPS=false
|
||||||
|
|
||||||
# OpenID Connect authentication configuration
|
# OpenID Connect authentication configuration
|
||||||
|
# Refer to https://www.bookstackapp.com/docs/admin/oidc-auth/
|
||||||
OIDC_NAME=SSO
|
OIDC_NAME=SSO
|
||||||
OIDC_DISPLAY_NAME_CLAIMS=name
|
OIDC_DISPLAY_NAME_CLAIMS=name
|
||||||
OIDC_CLIENT_ID=null
|
OIDC_CLIENT_ID=null
|
||||||
|
|||||||
7
.github/translators.txt
vendored
7
.github/translators.txt
vendored
@@ -199,3 +199,10 @@ M Nafis Al Mukhdi (mnafisalmukhdi1) :: Indonesian
|
|||||||
sulfo :: Danish
|
sulfo :: Danish
|
||||||
Raukze :: German
|
Raukze :: German
|
||||||
zygimantus :: Lithuanian
|
zygimantus :: Lithuanian
|
||||||
|
marinkaberg :: Russian
|
||||||
|
Vitaliy (gviabcua) :: Ukrainian
|
||||||
|
mannycarreiro :: Portuguese
|
||||||
|
Thiago Rafael Pereira de Carvalho (thiago.rafael) :: Portuguese, Brazilian
|
||||||
|
Ken Roger Bolgnes (kenbo124) :: Norwegian Bokmal
|
||||||
|
Nguyen Hung Phuong (hnwolf) :: Vietnamese
|
||||||
|
Umut ERGENE (umutergene67) :: Turkish
|
||||||
|
|||||||
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Start Database
|
- name: Start Database
|
||||||
run: |
|
run: |
|
||||||
sudo /etc/init.d/mysql start
|
sudo systemctl start mysql
|
||||||
|
|
||||||
- name: Setup Database
|
- name: Setup Database
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Start MySQL
|
- name: Start MySQL
|
||||||
run: |
|
run: |
|
||||||
sudo /etc/init.d/mysql start
|
sudo systemctl start mysql
|
||||||
|
|
||||||
- name: Create database & user
|
- name: Create database & user
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace BookStack\Actions;
|
|||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Auth\Permissions\PermissionService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
@@ -100,14 +101,14 @@ class ActivityService
|
|||||||
*/
|
*/
|
||||||
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
|
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
|
||||||
{
|
{
|
||||||
/** @var [string => int[]] $queryIds */
|
/** @var array<string, int[]> $queryIds */
|
||||||
$queryIds = [$entity->getMorphClass() => [$entity->id]];
|
$queryIds = [$entity->getMorphClass() => [$entity->id]];
|
||||||
|
|
||||||
if ($entity->isA('book')) {
|
if ($entity instanceof Book) {
|
||||||
$queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
|
$queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->scopes('visible')->pluck('id');
|
||||||
}
|
}
|
||||||
if ($entity->isA('book') || $entity->isA('chapter')) {
|
if ($entity instanceof Book || $entity instanceof Chapter) {
|
||||||
$queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id');
|
$queryIds[(new Page())->getMorphClass()] = $entity->pages()->scopes('visible')->pluck('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $this->activity->newQuery();
|
$query = $this->activity->newQuery();
|
||||||
@@ -132,7 +133,7 @@ class ActivityService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get latest activity for a user, Filtering out similar items.
|
* Get the latest activity for a user, Filtering out similar items.
|
||||||
*/
|
*/
|
||||||
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace BookStack\Auth\Access;
|
|||||||
|
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class ExternalBaseUserProvider implements UserProvider
|
class ExternalBaseUserProvider implements UserProvider
|
||||||
{
|
{
|
||||||
@@ -16,8 +17,6 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* LdapUserProvider constructor.
|
* LdapUserProvider constructor.
|
||||||
*
|
|
||||||
* @param $model
|
|
||||||
*/
|
*/
|
||||||
public function __construct(string $model)
|
public function __construct(string $model)
|
||||||
{
|
{
|
||||||
@@ -27,7 +26,7 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
/**
|
/**
|
||||||
* Create a new instance of the model.
|
* Create a new instance of the model.
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Database\Eloquent\Model
|
* @return Model
|
||||||
*/
|
*/
|
||||||
public function createModel()
|
public function createModel()
|
||||||
{
|
{
|
||||||
@@ -41,7 +40,7 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
*
|
*
|
||||||
* @param mixed $identifier
|
* @param mixed $identifier
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
* @return Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function retrieveById($identifier)
|
public function retrieveById($identifier)
|
||||||
{
|
{
|
||||||
@@ -54,7 +53,7 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
* @param mixed $identifier
|
* @param mixed $identifier
|
||||||
* @param string $token
|
* @param string $token
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
* @return Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function retrieveByToken($identifier, $token)
|
public function retrieveByToken($identifier, $token)
|
||||||
{
|
{
|
||||||
@@ -64,8 +63,8 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
/**
|
/**
|
||||||
* Update the "remember me" token for the given user in storage.
|
* Update the "remember me" token for the given user in storage.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @param Authenticatable $user
|
||||||
* @param string $token
|
* @param string $token
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
@@ -79,7 +78,7 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
*
|
*
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
*
|
*
|
||||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
* @return Authenticatable|null
|
||||||
*/
|
*/
|
||||||
public function retrieveByCredentials(array $credentials)
|
public function retrieveByCredentials(array $credentials)
|
||||||
{
|
{
|
||||||
@@ -94,8 +93,8 @@ class ExternalBaseUserProvider implements UserProvider
|
|||||||
/**
|
/**
|
||||||
* Validate a user against the given credentials.
|
* Validate a user against the given credentials.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
* @param Authenticatable $user
|
||||||
* @param array $credentials
|
* @param array $credentials
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class LdapService
|
|||||||
* Bind the system user to the LDAP connection using the given credentials
|
* Bind the system user to the LDAP connection using the given credentials
|
||||||
* otherwise anonymous access is attempted.
|
* otherwise anonymous access is attempted.
|
||||||
*
|
*
|
||||||
* @param $connection
|
* @param resource $connection
|
||||||
*
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -41,16 +41,18 @@ class OidcJwtSigningKey
|
|||||||
protected function loadFromPath(string $path)
|
protected function loadFromPath(string $path)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->key = PublicKeyLoader::load(
|
$key = PublicKeyLoader::load(
|
||||||
file_get_contents($path)
|
file_get_contents($path)
|
||||||
)->withPadding(RSA::SIGNATURE_PKCS1);
|
);
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
|
throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!($this->key instanceof RSA)) {
|
if (!$key instanceof RSA) {
|
||||||
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
|
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,14 +83,19 @@ class OidcJwtSigningKey
|
|||||||
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @var RSA $key */
|
$key = PublicKeyLoader::load([
|
||||||
$this->key = PublicKeyLoader::load([
|
|
||||||
'e' => new BigInteger(base64_decode($jwk['e']), 256),
|
'e' => new BigInteger(base64_decode($jwk['e']), 256),
|
||||||
'n' => new BigInteger(base64_decode($n), 256),
|
'n' => new BigInteger(base64_decode($n), 256),
|
||||||
])->withPadding(RSA::SIGNATURE_PKCS1);
|
]);
|
||||||
} catch (\Exception $exception) {
|
} catch (\Exception $exception) {
|
||||||
throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
|
throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$key instanceof RSA) {
|
||||||
|
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Illuminate\Support\Str;
|
|||||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||||
use Laravel\Socialite\Contracts\Provider;
|
use Laravel\Socialite\Contracts\Provider;
|
||||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||||
|
use Laravel\Socialite\Two\GoogleProvider;
|
||||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
|
||||||
@@ -278,7 +279,7 @@ class SocialAuthService
|
|||||||
{
|
{
|
||||||
$driver = $this->socialite->driver($driverName);
|
$driver = $this->socialite->driver($driverName);
|
||||||
|
|
||||||
if ($driverName === 'google' && config('services.google.select_account')) {
|
if ($driver instanceof GoogleProvider && config('services.google.select_account')) {
|
||||||
$driver->with(['prompt' => 'select_account']);
|
$driver->with(['prompt' => 'select_account']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -602,25 +602,35 @@ class PermissionService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter items that have entities set as a polymorphic relation.
|
* Filter items that have entities set as a polymorphic relation.
|
||||||
|
* For simplicity, this will not return results attached to draft pages.
|
||||||
|
* Draft pages should never really have related items though.
|
||||||
*
|
*
|
||||||
* @param Builder|QueryBuilder $query
|
* @param Builder|QueryBuilder $query
|
||||||
*/
|
*/
|
||||||
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||||
{
|
{
|
||||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||||
|
$pageMorphClass = (new Page())->getMorphClass();
|
||||||
|
|
||||||
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
$q = $query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
/** @var Builder $permissionQuery */
|
||||||
/** @var Builder $permissionQuery */
|
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
||||||
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||||
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||||
->whereColumn('joint_permissions.entity_type', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
->where('joint_permissions.action', '=', $action)
|
||||||
->where('action', '=', $action)
|
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
|
||||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
->where(function (QueryBuilder $query) {
|
||||||
->where(function (QueryBuilder $query) {
|
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
});
|
||||||
});
|
})->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||||
});
|
/** @var Builder $query */
|
||||||
|
$query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
|
||||||
|
->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
|
||||||
|
$query->select('id')->from('pages')
|
||||||
|
->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||||
|
->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
|
||||||
|
->where('pages.draft', '=', false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->clean();
|
$this->clean();
|
||||||
@@ -634,25 +644,39 @@ class PermissionService
|
|||||||
*/
|
*/
|
||||||
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
||||||
{
|
{
|
||||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
$fullEntityIdColumn = $tableName . '.' . $entityIdColumn;
|
||||||
$morphClass = app($entityClass)->getMorphClass();
|
$instance = new $entityClass();
|
||||||
|
$morphClass = $instance->getMorphClass();
|
||||||
|
|
||||||
$q = $query->where(function ($query) use ($tableDetails, $morphClass) {
|
$existsQuery = function ($permissionQuery) use ($fullEntityIdColumn, $morphClass) {
|
||||||
$query->where(function ($query) use (&$tableDetails, $morphClass) {
|
/** @var Builder $permissionQuery */
|
||||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
|
$permissionQuery->select('joint_permissions.role_id')->from('joint_permissions')
|
||||||
/** @var Builder $permissionQuery */
|
->whereColumn('joint_permissions.entity_id', '=', $fullEntityIdColumn)
|
||||||
$permissionQuery->select('id')->from('joint_permissions')
|
->where('joint_permissions.entity_type', '=', $morphClass)
|
||||||
->whereColumn('joint_permissions.entity_id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
->where('joint_permissions.action', '=', 'view')
|
||||||
->where('entity_type', '=', $morphClass)
|
->whereIn('joint_permissions.role_id', $this->getCurrentUserRoles())
|
||||||
->where('action', '=', 'view')
|
->where(function (QueryBuilder $query) {
|
||||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||||
->where(function (QueryBuilder $query) {
|
|
||||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
};
|
||||||
|
|
||||||
|
$q = $query->where(function ($query) use ($existsQuery, $fullEntityIdColumn) {
|
||||||
|
$query->whereExists($existsQuery)
|
||||||
|
->orWhere($fullEntityIdColumn, '=', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($instance instanceof Page) {
|
||||||
|
// Prevent visibility of non-owned draft pages
|
||||||
|
$q->whereExists(function (QueryBuilder $query) use ($fullEntityIdColumn) {
|
||||||
|
$query->select('id')->from('pages')
|
||||||
|
->whereColumn('pages.id', '=', $fullEntityIdColumn)
|
||||||
|
->where(function (QueryBuilder $query) {
|
||||||
|
$query->where('pages.draft', '=', false)
|
||||||
|
->orWhere('pages.owned_by', '=', $this->currentUser()->id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$this->clean();
|
$this->clean();
|
||||||
|
|
||||||
return $q;
|
return $q;
|
||||||
@@ -666,9 +690,9 @@ class PermissionService
|
|||||||
*/
|
*/
|
||||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||||
{
|
{
|
||||||
$query->where('has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
$query->where('joint_permissions.has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
||||||
$query->where('has_permission_own', '=', true)
|
$query->where('joint_permissions.has_permission_own', '=', true)
|
||||||
->where('owned_by', '=', $userIdToCheck);
|
->where('joint_permissions.owned_by', '=', $userIdToCheck);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace BookStack\Auth\Permissions;
|
|||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@@ -13,19 +14,15 @@ class RolePermission extends Model
|
|||||||
/**
|
/**
|
||||||
* The roles that belong to the permission.
|
* The roles that belong to the permission.
|
||||||
*/
|
*/
|
||||||
public function roles()
|
public function roles(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id');
|
return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the permission object by name.
|
* Get the permission object by name.
|
||||||
*
|
|
||||||
* @param $name
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public static function getByName($name)
|
public static function getByName(string $name): ?RolePermission
|
||||||
{
|
{
|
||||||
return static::where('name', '=', $name)->first();
|
return static::where('name', '=', $name)->first();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,16 @@ class UserRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the users with their permissions in a paginated format.
|
* Get all the users with their permissions in a paginated format.
|
||||||
|
* Note: Due to the use of email search this should only be used when
|
||||||
|
* user is assumed to be trusted. (Admin users).
|
||||||
|
* Email search can be abused to extract email addresses.
|
||||||
*/
|
*/
|
||||||
public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
|
public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$sort = $sortData['sort'];
|
$sort = $sortData['sort'];
|
||||||
|
|
||||||
$query = User::query()->select(['*'])
|
$query = User::query()->select(['*'])
|
||||||
->withLastActivityAt()
|
->scopes(['withLastActivityAt'])
|
||||||
->with(['roles', 'avatar'])
|
->with(['roles', 'avatar'])
|
||||||
->withCount('mfaValues')
|
->withCount('mfaValues')
|
||||||
->orderBy($sort, $sortData['order']);
|
->orderBy($sort, $sortData['order']);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class RegenerateSearch extends Command
|
|||||||
DB::setDefaultConnection($this->option('database'));
|
DB::setDefaultConnection($this->option('database'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total) {
|
$this->searchIndex->indexAllEntities(function (Entity $model, int $processed, int $total): void {
|
||||||
$this->info('Indexed ' . class_basename($model) . ' entries (' . $processed . '/' . $total . ')');
|
$this->info('Indexed ' . class_basename($model) . ' entries (' . $processed . '/' . $total . ')');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -79,53 +79,43 @@ class Book extends Entity implements HasCoverImage
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all pages within this book.
|
* Get all pages within this book.
|
||||||
*
|
|
||||||
* @return HasMany
|
|
||||||
*/
|
*/
|
||||||
public function pages()
|
public function pages(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Page::class);
|
return $this->hasMany(Page::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the direct child pages of this book.
|
* Get the direct child pages of this book.
|
||||||
*
|
|
||||||
* @return HasMany
|
|
||||||
*/
|
*/
|
||||||
public function directPages()
|
public function directPages(): HasMany
|
||||||
{
|
{
|
||||||
return $this->pages()->where('chapter_id', '=', '0');
|
return $this->pages()->where('chapter_id', '=', '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all chapters within this book.
|
* Get all chapters within this book.
|
||||||
*
|
|
||||||
* @return HasMany
|
|
||||||
*/
|
*/
|
||||||
public function chapters()
|
public function chapters(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Chapter::class);
|
return $this->hasMany(Chapter::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the shelves this book is contained within.
|
* Get the shelves this book is contained within.
|
||||||
*
|
|
||||||
* @return BelongsToMany
|
|
||||||
*/
|
*/
|
||||||
public function shelves()
|
public function shelves(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
|
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the direct child items within this book.
|
* Get the direct child items within this book.
|
||||||
*
|
|
||||||
* @return Collection
|
|
||||||
*/
|
*/
|
||||||
public function getDirectChildren(): Collection
|
public function getDirectChildren(): Collection
|
||||||
{
|
{
|
||||||
$pages = $this->directPages()->visible()->get();
|
$pages = $this->directPages()->scopes('visible')->get();
|
||||||
$chapters = $this->chapters()->visible()->get();
|
$chapters = $this->chapters()->scopes('visible')->get();
|
||||||
|
|
||||||
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
|||||||
*/
|
*/
|
||||||
public function visibleBooks(): BelongsToMany
|
public function visibleBooks(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->books()->visible();
|
return $this->books()->scopes('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class Chapter extends BookChild
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the pages that this chapter contains.
|
* Get the pages that this chapter contains.
|
||||||
|
*
|
||||||
|
* @return HasMany<Page>
|
||||||
*/
|
*/
|
||||||
public function pages(string $dir = 'ASC'): HasMany
|
public function pages(string $dir = 'ASC'): HasMany
|
||||||
{
|
{
|
||||||
@@ -50,7 +52,8 @@ class Chapter extends BookChild
|
|||||||
*/
|
*/
|
||||||
public function getVisiblePages(): Collection
|
public function getVisiblePages(): Collection
|
||||||
{
|
{
|
||||||
return $this->pages()->visible()
|
return $this->pages()
|
||||||
|
->scopes('visible')
|
||||||
->orderBy('draft', 'desc')
|
->orderBy('draft', 'desc')
|
||||||
->orderBy('priority', 'asc')
|
->orderBy('priority', 'asc')
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
namespace BookStack\Entities\Models;
|
namespace BookStack\Entities\Models;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
|
use BookStack\Interfaces\Deletable;
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property Model $deletable
|
* @property Deletable $deletable
|
||||||
*/
|
*/
|
||||||
class Deletion extends Model implements Loggable
|
class Deletion extends Model implements Loggable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use BookStack\Auth\Permissions\JointPermission;
|
|||||||
use BookStack\Entities\Tools\SearchIndex;
|
use BookStack\Entities\Tools\SearchIndex;
|
||||||
use BookStack\Entities\Tools\SlugGenerator;
|
use BookStack\Entities\Tools\SlugGenerator;
|
||||||
use BookStack\Facades\Permissions;
|
use BookStack\Facades\Permissions;
|
||||||
|
use BookStack\Interfaces\Deletable;
|
||||||
use BookStack\Interfaces\Favouritable;
|
use BookStack\Interfaces\Favouritable;
|
||||||
use BookStack\Interfaces\Sluggable;
|
use BookStack\Interfaces\Sluggable;
|
||||||
use BookStack\Interfaces\Viewable;
|
use BookStack\Interfaces\Viewable;
|
||||||
@@ -44,7 +45,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @method static Builder withLastView()
|
* @method static Builder withLastView()
|
||||||
* @method static Builder withViewCount()
|
* @method static Builder withViewCount()
|
||||||
*/
|
*/
|
||||||
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
use HasCreatorAndUpdater;
|
use HasCreatorAndUpdater;
|
||||||
@@ -120,11 +121,11 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
if (($entity instanceof BookChild) && $this instanceof Book) {
|
||||||
return $entity->book_id === $this->id;
|
return $entity->book_id === $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entity->isA('page') && $this->isA('chapter')) {
|
if ($entity instanceof Page && $this instanceof Chapter) {
|
||||||
return $entity->chapter_id === $this->id;
|
return $entity->chapter_id === $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +211,8 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
|||||||
/**
|
/**
|
||||||
* Check if this instance or class is a certain type of entity.
|
* Check if this instance or class is a certain type of entity.
|
||||||
* Examples of $type are 'page', 'book', 'chapter'.
|
* Examples of $type are 'page', 'book', 'chapter'.
|
||||||
|
*
|
||||||
|
* @deprecated Use instanceof instead.
|
||||||
*/
|
*/
|
||||||
public static function isA(string $type): bool
|
public static function isA(string $type): bool
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,10 +63,8 @@ class PageRevision extends Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the previous revision for the same page if existing.
|
* Get the previous revision for the same page if existing.
|
||||||
*
|
|
||||||
* @return \BookStack\Entities\PageRevision|null
|
|
||||||
*/
|
*/
|
||||||
public function getPrevious()
|
public function getPrevious(): ?PageRevision
|
||||||
{
|
{
|
||||||
$id = static::newQuery()->where('page_id', '=', $this->page_id)
|
$id = static::newQuery()->where('page_id', '=', $this->page_id)
|
||||||
->where('id', '<', $this->id)
|
->where('id', '<', $this->id)
|
||||||
@@ -84,11 +82,9 @@ class PageRevision extends Model
|
|||||||
* Included here to align with entities in similar use cases.
|
* Included here to align with entities in similar use cases.
|
||||||
* (Yup, Bit of an awkward hack).
|
* (Yup, Bit of an awkward hack).
|
||||||
*
|
*
|
||||||
* @param $type
|
* @deprecated Use instanceof instead.
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public static function isA($type)
|
public static function isA(string $type): bool
|
||||||
{
|
{
|
||||||
return $type === 'revision';
|
return $type === 'revision';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,10 +67,12 @@ class BaseRepo
|
|||||||
/**
|
/**
|
||||||
* Update the given items' cover image, or clear it.
|
* Update the given items' cover image, or clear it.
|
||||||
*
|
*
|
||||||
|
* @param Entity&HasCoverImage $entity
|
||||||
|
*
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function updateCoverImage(HasCoverImage $entity, ?UploadedFile $coverImage, bool $removeImage = false)
|
public function updateCoverImage($entity, ?UploadedFile $coverImage, bool $removeImage = false)
|
||||||
{
|
{
|
||||||
if ($coverImage) {
|
if ($coverImage) {
|
||||||
$this->imageRepo->destroyImage($entity->cover);
|
$this->imageRepo->destroyImage($entity->cover);
|
||||||
|
|||||||
@@ -69,9 +69,10 @@ class PageRepo
|
|||||||
*/
|
*/
|
||||||
public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
|
public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
|
||||||
{
|
{
|
||||||
|
/** @var ?PageRevision $revision */
|
||||||
$revision = PageRevision::query()
|
$revision = PageRevision::query()
|
||||||
->whereHas('page', function (Builder $query) {
|
->whereHas('page', function (Builder $query) {
|
||||||
$query->visible();
|
$query->scopes('visible');
|
||||||
})
|
})
|
||||||
->where('slug', '=', $pageSlug)
|
->where('slug', '=', $pageSlug)
|
||||||
->where('type', '=', 'version')
|
->where('type', '=', 'version')
|
||||||
@@ -80,7 +81,7 @@ class PageRepo
|
|||||||
->with('page')
|
->with('page')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
return $revision ? $revision->page : null;
|
return $revision->page ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -290,6 +291,8 @@ class PageRepo
|
|||||||
public function restoreRevision(Page $page, int $revisionId): Page
|
public function restoreRevision(Page $page, int $revisionId): Page
|
||||||
{
|
{
|
||||||
$page->revision_count++;
|
$page->revision_count++;
|
||||||
|
|
||||||
|
/** @var PageRevision $revision */
|
||||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||||
|
|
||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
@@ -334,7 +337,8 @@ class PageRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
|
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
|
||||||
$page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
|
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
|
||||||
|
$page->changeBook($newBookId);
|
||||||
$page->rebuildPermissions();
|
$page->rebuildPermissions();
|
||||||
|
|
||||||
Activity::addForEntity($page, ActivityType::PAGE_MOVE);
|
Activity::addForEntity($page, ActivityType::PAGE_MOVE);
|
||||||
@@ -406,7 +410,7 @@ class PageRepo
|
|||||||
*/
|
*/
|
||||||
protected function changeParent(Page $page, Entity $parent)
|
protected function changeParent(Page $page, Entity $parent)
|
||||||
{
|
{
|
||||||
$book = ($parent instanceof Book) ? $parent : $parent->book;
|
$book = ($parent instanceof Chapter) ? $parent->book : $parent;
|
||||||
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
|
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
|
||||||
$page->save();
|
$page->save();
|
||||||
|
|
||||||
@@ -467,6 +471,7 @@ class PageRepo
|
|||||||
{
|
{
|
||||||
$parent = $page->getParent();
|
$parent = $page->getParent();
|
||||||
if ($parent instanceof Chapter) {
|
if ($parent instanceof Chapter) {
|
||||||
|
/** @var ?Page $lastPage */
|
||||||
$lastPage = $parent->pages('desc')->first();
|
$lastPage = $parent->pages('desc')->first();
|
||||||
|
|
||||||
return $lastPage ? $lastPage->priority + 1 : 0;
|
return $lastPage ? $lastPage->priority + 1 : 0;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class BookContents
|
|||||||
$all->each(function (Entity $entity) use ($renderPages) {
|
$all->each(function (Entity $entity) use ($renderPages) {
|
||||||
$entity->setRelation('book', $this->book);
|
$entity->setRelation('book', $this->book);
|
||||||
|
|
||||||
if ($renderPages && $entity->isA('page')) {
|
if ($renderPages && $entity instanceof Page) {
|
||||||
$entity->html = (new PageContent($entity))->render();
|
$entity->html = (new PageContent($entity))->render();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -151,7 +151,7 @@ class BookContents
|
|||||||
|
|
||||||
$priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
|
$priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
|
||||||
$bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
|
$bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
|
||||||
$chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
|
$chapterChanged = ($model instanceof Page) && intval($model->chapter_id) !== $sortMapItem->parentChapter;
|
||||||
|
|
||||||
if ($bookChanged) {
|
if ($bookChanged) {
|
||||||
$model->changeBook($sortMapItem->book);
|
$model->changeBook($sortMapItem->book);
|
||||||
|
|||||||
@@ -7,21 +7,24 @@ use BookStack\Entities\Models\Chapter;
|
|||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use DomPDF;
|
use DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
|
use DOMXPath;
|
||||||
use Exception;
|
use Exception;
|
||||||
use SnappyPDF;
|
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class ExportFormatter
|
class ExportFormatter
|
||||||
{
|
{
|
||||||
protected $imageService;
|
protected $imageService;
|
||||||
|
protected $pdfGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExportService constructor.
|
* ExportService constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(ImageService $imageService)
|
public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator)
|
||||||
{
|
{
|
||||||
$this->imageService = $imageService;
|
$this->imageService = $imageService;
|
||||||
|
$this->pdfGenerator = $pdfGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,16 +142,40 @@ class ExportFormatter
|
|||||||
*/
|
*/
|
||||||
protected function htmlToPdf(string $html): string
|
protected function htmlToPdf(string $html): string
|
||||||
{
|
{
|
||||||
$containedHtml = $this->containHtml($html);
|
$html = $this->containHtml($html);
|
||||||
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
|
$html = $this->replaceIframesWithLinks($html);
|
||||||
if ($useWKHTML) {
|
|
||||||
$pdf = SnappyPDF::loadHTML($containedHtml);
|
return $this->pdfGenerator->fromHtml($html);
|
||||||
$pdf->setOption('print-media-type', true);
|
}
|
||||||
} else {
|
|
||||||
$pdf = DomPDF::loadHTML($containedHtml);
|
/**
|
||||||
|
* Within the given HTML content, replace any iframe elements
|
||||||
|
* with anchor links within paragraph blocks.
|
||||||
|
*/
|
||||||
|
protected function replaceIframesWithLinks(string $html): string
|
||||||
|
{
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xPath = new DOMXPath($doc);
|
||||||
|
|
||||||
|
$iframes = $xPath->query('//iframe');
|
||||||
|
/** @var DOMElement $iframe */
|
||||||
|
foreach ($iframes as $iframe) {
|
||||||
|
$link = $iframe->getAttribute('src');
|
||||||
|
if (strpos($link, '//') === 0) {
|
||||||
|
$link = 'https:' . $link;
|
||||||
|
}
|
||||||
|
|
||||||
|
$anchor = $doc->createElement('a', $link);
|
||||||
|
$anchor->setAttribute('href', $link);
|
||||||
|
$paragraph = $doc->createElement('p');
|
||||||
|
$paragraph->appendChild($anchor);
|
||||||
|
$iframe->parentNode->replaceChild($paragraph, $iframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $pdf->output();
|
return $doc->saveHTML();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class NextPreviousContentLocator
|
|||||||
/** @var Entity $item */
|
/** @var Entity $item */
|
||||||
foreach ($bookTree->all() as $item) {
|
foreach ($bookTree->all() as $item) {
|
||||||
$flatOrdered->push($item);
|
$flatOrdered->push($item);
|
||||||
$childPages = $item->visible_pages ?? [];
|
$childPages = $item->getAttribute('visible_pages') ?? [];
|
||||||
$flatOrdered = $flatOrdered->concat($childPages);
|
$flatOrdered = $flatOrdered->concat($childPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use BookStack\Uploads\ImageRepo;
|
|||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use BookStack\Util\HtmlContentFilter;
|
use BookStack\Util\HtmlContentFilter;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
|
use DOMElement;
|
||||||
|
use DOMNode;
|
||||||
use DOMNodeList;
|
use DOMNodeList;
|
||||||
use DOMXPath;
|
use DOMXPath;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -156,7 +158,7 @@ class PageContent
|
|||||||
/**
|
/**
|
||||||
* Parse a base64 image URI into the data and extension.
|
* Parse a base64 image URI into the data and extension.
|
||||||
*
|
*
|
||||||
* @return array{extension: array, data: string}
|
* @return array{extension: string, data: string}
|
||||||
*/
|
*/
|
||||||
protected function parseBase64ImageUri(string $uri): array
|
protected function parseBase64ImageUri(string $uri): array
|
||||||
{
|
{
|
||||||
@@ -193,6 +195,15 @@ class PageContent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set ids on nested header nodes
|
||||||
|
$nestedHeaders = $xPath->query('//body//*//h1|//body//*//h2|//body//*//h3|//body//*//h4|//body//*//h5|//body//*//h6');
|
||||||
|
foreach ($nestedHeaders as $nestedHeader) {
|
||||||
|
[$oldId, $newId] = $this->setUniqueId($nestedHeader, $idMap);
|
||||||
|
if ($newId && $newId !== $oldId) {
|
||||||
|
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure no duplicate ids within child items
|
// Ensure no duplicate ids within child items
|
||||||
$idElems = $xPath->query('//body//*//*[@id]');
|
$idElems = $xPath->query('//body//*//*[@id]');
|
||||||
foreach ($idElems as $domElem) {
|
foreach ($idElems as $domElem) {
|
||||||
@@ -228,9 +239,9 @@ class PageContent
|
|||||||
* A map for existing ID's should be passed in to check for current existence.
|
* A map for existing ID's should be passed in to check for current existence.
|
||||||
* Returns a pair of strings in the format [old_id, new_id].
|
* Returns a pair of strings in the format [old_id, new_id].
|
||||||
*/
|
*/
|
||||||
protected function setUniqueId(\DOMNode $element, array &$idMap): array
|
protected function setUniqueId(DOMNode $element, array &$idMap): array
|
||||||
{
|
{
|
||||||
if (get_class($element) !== 'DOMElement') {
|
if (!$element instanceof DOMElement) {
|
||||||
return ['', ''];
|
return ['', ''];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +253,7 @@ class PageContent
|
|||||||
return [$existingId, $existingId];
|
return [$existingId, $existingId];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an unique id for the element
|
// Create a unique id for the element
|
||||||
// Uses the content as a basis to ensure output is the same every time
|
// Uses the content as a basis to ensure output is the same every time
|
||||||
// the same content is passed through.
|
// the same content is passed through.
|
||||||
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
|
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
|
||||||
@@ -312,7 +323,7 @@ class PageContent
|
|||||||
*/
|
*/
|
||||||
protected function headerNodesToLevelList(DOMNodeList $nodeList): array
|
protected function headerNodesToLevelList(DOMNodeList $nodeList): array
|
||||||
{
|
{
|
||||||
$tree = collect($nodeList)->map(function ($header) {
|
$tree = collect($nodeList)->map(function (DOMElement $header) {
|
||||||
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
|
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
|
||||||
$text = mb_substr($text, 0, 100);
|
$text = mb_substr($text, 0, 100);
|
||||||
|
|
||||||
|
|||||||
26
app/Entities/Tools/PdfGenerator.php
Normal file
26
app/Entities/Tools/PdfGenerator.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Entities\Tools;
|
||||||
|
|
||||||
|
use Barryvdh\DomPDF\Facade as DomPDF;
|
||||||
|
use Barryvdh\Snappy\Facades\SnappyPdf;
|
||||||
|
|
||||||
|
class PdfGenerator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generate PDF content from the given HTML content.
|
||||||
|
*/
|
||||||
|
public function fromHtml(string $html): string
|
||||||
|
{
|
||||||
|
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
|
||||||
|
|
||||||
|
if ($useWKHTML) {
|
||||||
|
$pdf = SnappyPDF::loadHTML($html);
|
||||||
|
$pdf->setOption('print-media-type', true);
|
||||||
|
} else {
|
||||||
|
$pdf = DomPDF::loadHTML($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pdf->output();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use BookStack\Entities\Models\Page;
|
|||||||
use BookStack\Entities\Models\SearchTerm;
|
use BookStack\Entities\Models\SearchTerm;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMNode;
|
use DOMNode;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class SearchIndex
|
class SearchIndex
|
||||||
@@ -67,7 +68,7 @@ class SearchIndex
|
|||||||
* - The number that have been processed so far.
|
* - The number that have been processed so far.
|
||||||
* - The total number of that model to be processed.
|
* - The total number of that model to be processed.
|
||||||
*
|
*
|
||||||
* @param callable(Entity, int, int)|null $progressCallback
|
* @param callable(Entity, int, int):void|null $progressCallback
|
||||||
*/
|
*/
|
||||||
public function indexAllEntities(?callable $progressCallback = null)
|
public function indexAllEntities(?callable $progressCallback = null)
|
||||||
{
|
{
|
||||||
@@ -76,7 +77,9 @@ class SearchIndex
|
|||||||
foreach ($this->entityProvider->all() as $entityModel) {
|
foreach ($this->entityProvider->all() as $entityModel) {
|
||||||
$indexContentField = $entityModel instanceof Page ? 'html' : 'description';
|
$indexContentField = $entityModel instanceof Page ? 'html' : 'description';
|
||||||
$selectFields = ['id', 'name', $indexContentField];
|
$selectFields = ['id', 'name', $indexContentField];
|
||||||
$total = $entityModel->newQuery()->withTrashed()->count();
|
/** @var Builder<Entity> $query */
|
||||||
|
$query = $entityModel->newQuery();
|
||||||
|
$total = $query->withTrashed()->count();
|
||||||
$chunkSize = 250;
|
$chunkSize = 250;
|
||||||
$processed = 0;
|
$processed = 0;
|
||||||
|
|
||||||
@@ -223,7 +226,7 @@ class SearchIndex
|
|||||||
if ($entity instanceof Page) {
|
if ($entity instanceof Page) {
|
||||||
$bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html);
|
$bodyTermsMap = $this->generateTermScoreMapFromHtml($entity->html);
|
||||||
} else {
|
} else {
|
||||||
$bodyTermsMap = $this->generateTermScoreMapFromText($entity->description ?? '', $entity->searchFactor);
|
$bodyTermsMap = $this->generateTermScoreMapFromText($entity->getAttribute('description') ?? '', $entity->searchFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap, $tagTermsMap);
|
$mergedScoreMap = $this->mergeTermScoreMaps($nameTermsMap, $bodyTermsMap, $tagTermsMap);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use BookStack\Entities\Models\BookChild;
|
|||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Models\SearchTerm;
|
use BookStack\Entities\Models\SearchTerm;
|
||||||
|
use Illuminate\Database\Connection;
|
||||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
@@ -144,13 +145,13 @@ class SearchRunner
|
|||||||
|
|
||||||
if ($entityModelInstance instanceof BookChild) {
|
if ($entityModelInstance instanceof BookChild) {
|
||||||
$relations['book'] = function (BelongsTo $query) {
|
$relations['book'] = function (BelongsTo $query) {
|
||||||
$query->visible();
|
$query->scopes('visible');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entityModelInstance instanceof Page) {
|
if ($entityModelInstance instanceof Page) {
|
||||||
$relations['chapter'] = function (BelongsTo $query) {
|
$relations['chapter'] = function (BelongsTo $query) {
|
||||||
$query->visible();
|
$query->scopes('visible');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,7 +357,9 @@ class SearchRunner
|
|||||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||||
// search the value as a string which prevents being able to do number-based operations
|
// search the value as a string which prevents being able to do number-based operations
|
||||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
/** @var Connection $connection */
|
||||||
|
$connection = $query->getConnection();
|
||||||
|
$tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'");
|
||||||
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
||||||
} else {
|
} else {
|
||||||
$query->where('value', $tagOperator, $tagValue);
|
$query->where('value', $tagOperator, $tagValue);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Tools;
|
|||||||
use BookStack\Entities\EntityProvider;
|
use BookStack\Entities\EntityProvider;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ class SiblingFetcher
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Page in book or chapter
|
// Page in book or chapter
|
||||||
if (($entity instanceof Page && !$entity->chapter) || $entity->isA('chapter')) {
|
if (($entity instanceof Page && !$entity->chapter) || $entity instanceof Chapter) {
|
||||||
$entities = $entity->book->getDirectChildren();
|
$entities = $entity->book->getDirectChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use BookStack\Facades\Activity;
|
|||||||
use BookStack\Uploads\AttachmentService;
|
use BookStack\Uploads\AttachmentService;
|
||||||
use BookStack\Uploads\ImageService;
|
use BookStack\Uploads\ImageService;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class TrashCan
|
class TrashCan
|
||||||
@@ -141,11 +142,9 @@ class TrashCan
|
|||||||
{
|
{
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$pages = $chapter->pages()->withTrashed()->get();
|
$pages = $chapter->pages()->withTrashed()->get();
|
||||||
if (count($pages)) {
|
foreach ($pages as $page) {
|
||||||
foreach ($pages as $page) {
|
$this->destroyPage($page);
|
||||||
$this->destroyPage($page);
|
$count++;
|
||||||
$count++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->destroyCommonRelations($chapter);
|
$this->destroyCommonRelations($chapter);
|
||||||
@@ -183,9 +182,10 @@ class TrashCan
|
|||||||
{
|
{
|
||||||
$counts = [];
|
$counts = [];
|
||||||
|
|
||||||
/** @var Entity $instance */
|
|
||||||
foreach ((new EntityProvider())->all() as $key => $instance) {
|
foreach ((new EntityProvider())->all() as $key => $instance) {
|
||||||
$counts[$key] = $instance->newQuery()->onlyTrashed()->count();
|
/** @var Builder<Entity> $query */
|
||||||
|
$query = $instance->newQuery();
|
||||||
|
$counts[$key] = $query->onlyTrashed()->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $counts;
|
return $counts;
|
||||||
@@ -235,13 +235,15 @@ class TrashCan
|
|||||||
{
|
{
|
||||||
$shouldRestore = true;
|
$shouldRestore = true;
|
||||||
$restoreCount = 0;
|
$restoreCount = 0;
|
||||||
$parent = $deletion->deletable->getParent();
|
|
||||||
|
|
||||||
if ($parent && $parent->trashed()) {
|
if ($deletion->deletable instanceof Entity) {
|
||||||
$shouldRestore = false;
|
$parent = $deletion->deletable->getParent();
|
||||||
|
if ($parent && $parent->trashed()) {
|
||||||
|
$shouldRestore = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($shouldRestore) {
|
if ($deletion->deletable instanceof Entity && $shouldRestore) {
|
||||||
$restoreCount = $this->restoreEntity($deletion->deletable);
|
$restoreCount = $this->restoreEntity($deletion->deletable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,9 +344,9 @@ class TrashCan
|
|||||||
$entity->deletions()->delete();
|
$entity->deletions()->delete();
|
||||||
$entity->favourites()->delete();
|
$entity->favourites()->delete();
|
||||||
|
|
||||||
if ($entity instanceof HasCoverImage && $entity->cover) {
|
if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
|
||||||
$imageService = app()->make(ImageService::class);
|
$imageService = app()->make(ImageService::class);
|
||||||
$imageService->destroy($entity->cover);
|
$imageService->destroy($entity->cover()->first());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace BookStack\Exceptions;
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Auth\AuthenticationException;
|
use Illuminate\Auth\AuthenticationException;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -75,15 +76,20 @@ class Handler extends ExceptionHandler
|
|||||||
/**
|
/**
|
||||||
* Render an exception when the API is in use.
|
* Render an exception when the API is in use.
|
||||||
*/
|
*/
|
||||||
protected function renderApiException(Exception $e): JsonResponse
|
protected function renderApiException(Throwable $e): JsonResponse
|
||||||
{
|
{
|
||||||
$code = $e->getCode() === 0 ? 500 : $e->getCode();
|
$code = 500;
|
||||||
$headers = [];
|
$headers = [];
|
||||||
|
|
||||||
if ($e instanceof HttpException) {
|
if ($e instanceof HttpException) {
|
||||||
$code = $e->getStatusCode();
|
$code = $e->getStatusCode();
|
||||||
$headers = $e->getHeaders();
|
$headers = $e->getHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($e instanceof ModelNotFoundException) {
|
||||||
|
$code = 404;
|
||||||
|
}
|
||||||
|
|
||||||
$responseData = [
|
$responseData = [
|
||||||
'error' => [
|
'error' => [
|
||||||
'message' => $e->getMessage(),
|
'message' => $e->getMessage(),
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class BookshelfApiController extends ApiController
|
|||||||
$shelf = Bookshelf::visible()->with([
|
$shelf = Bookshelf::visible()->with([
|
||||||
'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
|
'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
|
||||||
'books' => function (BelongsToMany $query) {
|
'books' => function (BelongsToMany $query) {
|
||||||
$query->visible()->get(['id', 'name', 'slug']);
|
$query->scopes('visible')->get(['id', 'name', 'slug']);
|
||||||
},
|
},
|
||||||
])->findOrFail($id);
|
])->findOrFail($id);
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ChapterApiController extends ApiController
|
|||||||
public function read(string $id)
|
public function read(string $id)
|
||||||
{
|
{
|
||||||
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
|
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
|
||||||
$query->visible()->get(['id', 'name', 'slug']);
|
$query->scopes('visible')->get(['id', 'name', 'slug']);
|
||||||
}])->findOrFail($id);
|
}])->findOrFail($id);
|
||||||
|
|
||||||
return response()->json($chapter);
|
return response()->json($chapter);
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ namespace BookStack\Http\Controllers\Api;
|
|||||||
|
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Tools\SearchOptions;
|
use BookStack\Entities\Tools\SearchOptions;
|
||||||
|
use BookStack\Entities\Tools\SearchResultsFormatter;
|
||||||
use BookStack\Entities\Tools\SearchRunner;
|
use BookStack\Entities\Tools\SearchRunner;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SearchApiController extends ApiController
|
class SearchApiController extends ApiController
|
||||||
{
|
{
|
||||||
protected $searchRunner;
|
protected $searchRunner;
|
||||||
|
protected $resultsFormatter;
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'all' => [
|
'all' => [
|
||||||
@@ -19,9 +21,10 @@ class SearchApiController extends ApiController
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(SearchRunner $searchRunner)
|
public function __construct(SearchRunner $searchRunner, SearchResultsFormatter $resultsFormatter)
|
||||||
{
|
{
|
||||||
$this->searchRunner = $searchRunner;
|
$this->searchRunner = $searchRunner;
|
||||||
|
$this->resultsFormatter = $resultsFormatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +48,7 @@ class SearchApiController extends ApiController
|
|||||||
$count = min(intval($request->get('count', '0')) ?: 20, 100);
|
$count = min(intval($request->get('count', '0')) ?: 20, 100);
|
||||||
|
|
||||||
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
$results = $this->searchRunner->searchEntities($options, 'all', $page, $count);
|
||||||
|
$this->resultsFormatter->format($results['results']->all(), $options);
|
||||||
|
|
||||||
/** @var Entity $result */
|
/** @var Entity $result */
|
||||||
foreach ($results['results'] as $result) {
|
foreach ($results['results'] as $result) {
|
||||||
@@ -52,9 +56,14 @@ class SearchApiController extends ApiController
|
|||||||
'id', 'name', 'slug', 'book_id',
|
'id', 'name', 'slug', 'book_id',
|
||||||
'chapter_id', 'draft', 'template',
|
'chapter_id', 'draft', 'template',
|
||||||
'created_at', 'updated_at',
|
'created_at', 'updated_at',
|
||||||
'tags', 'type',
|
'tags', 'type', 'preview_html', 'url',
|
||||||
]);
|
]);
|
||||||
$result->setAttribute('type', $result->getType());
|
$result->setAttribute('type', $result->getType());
|
||||||
|
$result->setAttribute('url', $result->getUrl());
|
||||||
|
$result->setAttribute('preview_html', [
|
||||||
|
'name' => (string) $result->getAttribute('preview_name'),
|
||||||
|
'content' => (string) $result->getAttribute('preview_content'),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class BookController extends Controller
|
|||||||
{
|
{
|
||||||
$book = $this->bookRepo->getBySlug($slug);
|
$book = $this->bookRepo->getBySlug($slug);
|
||||||
$bookChildren = (new BookContents($book))->getTree(true);
|
$bookChildren = (new BookContents($book))->getTree(true);
|
||||||
$bookParentShelves = $book->shelves()->visible()->get();
|
$bookParentShelves = $book->shelves()->scopes('visible')->get();
|
||||||
|
|
||||||
View::incrementFor($book);
|
View::incrementFor($book);
|
||||||
if ($request->has('shelf')) {
|
if ($request->has('shelf')) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class HomeController extends Controller
|
|||||||
$recentlyUpdatedPages = Page::visible()->with('book')
|
$recentlyUpdatedPages = Page::visible()->with('book')
|
||||||
->where('draft', false)
|
->where('draft', false)
|
||||||
->orderBy('updated_at', 'desc')
|
->orderBy('updated_at', 'desc')
|
||||||
->take($favourites->count() > 0 ? 6 : 12)
|
->take($favourites->count() > 0 ? 5 : 10)
|
||||||
->select(Page::$listAttributes)
|
->select(Page::$listAttributes)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class RecycleBinController extends Controller
|
|||||||
$searching = false;
|
$searching = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var ?Deletion $parentDeletion */
|
/** @var ?Deletion $parentDeletion */
|
||||||
$parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first();
|
$parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class UserSearchController extends Controller
|
class UserSearchController extends Controller
|
||||||
@@ -14,19 +13,27 @@ class UserSearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function forSelect(Request $request)
|
public function forSelect(Request $request)
|
||||||
{
|
{
|
||||||
|
$hasPermission = signedInUser() && (
|
||||||
|
userCan('users-manage')
|
||||||
|
|| userCan('restrictions-manage-own')
|
||||||
|
|| userCan('restrictions-manage-all')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$hasPermission) {
|
||||||
|
$this->showPermissionError();
|
||||||
|
}
|
||||||
|
|
||||||
$search = $request->get('search', '');
|
$search = $request->get('search', '');
|
||||||
$query = User::query()->orderBy('name', 'desc')
|
$query = User::query()
|
||||||
|
->orderBy('name', 'asc')
|
||||||
->take(20);
|
->take(20);
|
||||||
|
|
||||||
if (!empty($search)) {
|
if (!empty($search)) {
|
||||||
$query->where(function (Builder $query) use ($search) {
|
$query->where('name', 'like', '%' . $search . '%');
|
||||||
$query->where('email', 'like', '%' . $search . '%')
|
|
||||||
->orWhere('name', 'like', '%' . $search . '%');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$users = $query->get();
|
return view('form.user-select-list', [
|
||||||
|
'users' => $query->get(),
|
||||||
return view('form.user-select-list', compact('users'));
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ApiAuthenticate
|
|||||||
// Return if the user is already found to be signed in via session-based auth.
|
// Return if the user is already found to be signed in via session-based auth.
|
||||||
// This is to make it easy to browser the API via browser after just logging into the system.
|
// This is to make it easy to browser the API via browser after just logging into the system.
|
||||||
if (signedInUser() || session()->isStarted()) {
|
if (signedInUser() || session()->isStarted()) {
|
||||||
if (!user()->can('access-api')) {
|
if (!$this->sessionUserHasApiAccess()) {
|
||||||
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
|
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +49,16 @@ class ApiAuthenticate
|
|||||||
auth()->authenticate();
|
auth()->authenticate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the active session user has API access.
|
||||||
|
*/
|
||||||
|
protected function sessionUserHasApiAccess(): bool
|
||||||
|
{
|
||||||
|
$hasApiPermission = user()->can('access-api');
|
||||||
|
|
||||||
|
return $hasApiPermission && hasAppAccess();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide a standard API unauthorised response.
|
* Provide a standard API unauthorised response.
|
||||||
*/
|
*/
|
||||||
|
|||||||
14
app/Interfaces/Deletable.php
Normal file
14
app/Interfaces/Deletable.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Interfaces;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A model that can be deleted in a manner that deletions
|
||||||
|
* are tracked to be part of the recycle bin system.
|
||||||
|
*/
|
||||||
|
interface Deletable
|
||||||
|
{
|
||||||
|
public function deletions(): MorphMany;
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
namespace BookStack\Theming;
|
namespace BookStack\Theming;
|
||||||
|
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Auth\Access\SocialAuthService;
|
||||||
|
use Illuminate\Console\Application;
|
||||||
|
use Illuminate\Console\Application as Artisan;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
|
||||||
class ThemeService
|
class ThemeService
|
||||||
{
|
{
|
||||||
@@ -43,6 +46,16 @@ class ThemeService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new custom artisan command to be available.
|
||||||
|
*/
|
||||||
|
public function registerCommand(Command $command)
|
||||||
|
{
|
||||||
|
Artisan::starting(function (Application $application) use ($command) {
|
||||||
|
$application->addCommands([$command]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read any actions from the set theme path if the 'functions.php' file exists.
|
* Read any actions from the set theme path if the 'functions.php' file exists.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -103,7 +103,10 @@ class ImageRepo
|
|||||||
if ($filterType === 'page') {
|
if ($filterType === 'page') {
|
||||||
$query->where('uploaded_to', '=', $contextPage->id);
|
$query->where('uploaded_to', '=', $contextPage->id);
|
||||||
} elseif ($filterType === 'book') {
|
} elseif ($filterType === 'book') {
|
||||||
$validPageIds = $contextPage->book->pages()->visible()->pluck('id')->toArray();
|
$validPageIds = $contextPage->book->pages()
|
||||||
|
->scopes('visible')
|
||||||
|
->pluck('id')
|
||||||
|
->toArray();
|
||||||
$query->whereIn('uploaded_to', $validPageIds);
|
$query->whereIn('uploaded_to', $validPageIds);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
1083
composer.lock
generated
1083
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,11 @@
|
|||||||
"created_at": "2021-11-14T15:57:35.000000Z",
|
"created_at": "2021-11-14T15:57:35.000000Z",
|
||||||
"updated_at": "2021-11-14T15:57:35.000000Z",
|
"updated_at": "2021-11-14T15:57:35.000000Z",
|
||||||
"type": "chapter",
|
"type": "chapter",
|
||||||
|
"url": "https://example.com/books/my-book/chapter/a-chapter-for-cats",
|
||||||
|
"preview_html": {
|
||||||
|
"name": "A chapter for <strong>cats</strong>",
|
||||||
|
"content": "...once a bunch of <strong>cats</strong> named tony...behaviour of <strong>cats</strong> is unsuitable"
|
||||||
|
},
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -21,6 +26,11 @@
|
|||||||
"created_at": "2021-05-15T16:28:10.000000Z",
|
"created_at": "2021-05-15T16:28:10.000000Z",
|
||||||
"updated_at": "2021-11-14T15:56:49.000000Z",
|
"updated_at": "2021-11-14T15:56:49.000000Z",
|
||||||
"type": "page",
|
"type": "page",
|
||||||
|
"url": "https://example.com/books/my-book/page/the-hows-and-whys-of-cats",
|
||||||
|
"preview_html": {
|
||||||
|
"name": "The hows and whys of <strong>cats</strong>",
|
||||||
|
"content": "...people ask why <strong>cats</strong>? but there are...the reason that <strong>cats</strong> are fast are due to..."
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
{
|
||||||
"name": "Animal",
|
"name": "Animal",
|
||||||
@@ -45,6 +55,11 @@
|
|||||||
"created_at": "2020-11-29T21:55:07.000000Z",
|
"created_at": "2020-11-29T21:55:07.000000Z",
|
||||||
"updated_at": "2021-11-14T16:02:39.000000Z",
|
"updated_at": "2021-11-14T16:02:39.000000Z",
|
||||||
"type": "page",
|
"type": "page",
|
||||||
|
"url": "https://example.com/books/my-book/page/how-advanced-are-cats",
|
||||||
|
"preview_html": {
|
||||||
|
"name": "How advanced are <strong>cats</strong>?",
|
||||||
|
"content": "<strong>cats</strong> are some of the most advanced animals in the world."
|
||||||
|
},
|
||||||
"tags": []
|
"tags": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ WARNING: This system is currently in alpha so may incur changes. Once we've gath
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
*[Video Guide](https://www.youtube.com/watch?v=YVbpm_35crQ)*
|
||||||
|
|
||||||
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
|
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
|
||||||
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
|
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
|
||||||
|
|
||||||
@@ -50,6 +52,23 @@ This method allows you to register a custom social authentication driver within
|
|||||||
|
|
||||||
*See "Custom Socialite Service Example" below.*
|
*See "Custom Socialite Service Example" below.*
|
||||||
|
|
||||||
|
### `Theme::registerCommand`
|
||||||
|
|
||||||
|
This method allows you to register a custom command which can then be used via the artisan console.
|
||||||
|
|
||||||
|
**Arguments**
|
||||||
|
- string $driverName
|
||||||
|
- array $config
|
||||||
|
- string $socialiteHandler
|
||||||
|
|
||||||
|
**Example**
|
||||||
|
|
||||||
|
*See "Custom Command Registration Example" below for a more detailed example.*
|
||||||
|
|
||||||
|
```php
|
||||||
|
Theme::registerCommand(new SayHelloCommand());
|
||||||
|
```
|
||||||
|
|
||||||
## Available Events
|
## Available Events
|
||||||
|
|
||||||
All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on GitHub here](https://github.com/BookStackApp/BookStack/blob/release/app/Theming/ThemeEvents.php).
|
All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on GitHub here](https://github.com/BookStackApp/BookStack/blob/release/app/Theming/ThemeEvents.php).
|
||||||
@@ -77,6 +96,33 @@ Theme::listen(ThemeEvents::APP_BOOT, function($app) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Custom Command Registration Example
|
||||||
|
|
||||||
|
The logical theme system supports adding custom [artisan commands](https://laravel.com/docs/8.x/artisan) to BookStack.
|
||||||
|
These can be registered in your `functions.php` file by calling `Theme::registerCommand($command)`, where `$command` is an instance of `\Symfony\Component\Console\Command\Command`.
|
||||||
|
|
||||||
|
Below is an example of registering a command that could then be ran using `php artisan bookstack:meow` on the command line.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use BookStack\Facades\Theme;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MeowCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bookstack:meow';
|
||||||
|
protected $description = 'Say meow on the command line';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->line('Meow there!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Theme::registerCommand(new MeowCommand);
|
||||||
|
```
|
||||||
|
|
||||||
## Custom Socialite Service Example
|
## Custom Socialite Service Example
|
||||||
|
|
||||||
The below shows an example of adding a custom reddit socialite service to BookStack.
|
The below shows an example of adding a custom reddit socialite service to BookStack.
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ This theme system itself is maintained and supported but usages of this system,
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
*[Video Guide](https://www.youtube.com/watch?v=gLy_2GBse48)*
|
||||||
|
|
||||||
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
|
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
|
||||||
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
|
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
|
||||||
|
|
||||||
@@ -28,4 +30,4 @@ As an example, Say I wanted to change 'Search' to 'Find'; Within a `themes/<them
|
|||||||
return [
|
return [
|
||||||
'search' => 'find',
|
'search' => 'find',
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|||||||
2
public/dist/export-styles.css
vendored
2
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles.css
vendored
2
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico presionando en el siguiente botón:',
|
'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico presionando en el siguiente botón:',
|
||||||
'email_confirm_action' => 'Confirmar correo electrónico',
|
'email_confirm_action' => 'Confirmar correo electrónico',
|
||||||
'email_confirm_send_error' => 'Se pidió confirmación de correo electrónico pero el sistema no pudo enviar el correo electrónico. Contacte al administrador para asegurarse que el correo electrónico está configurado correctamente.',
|
'email_confirm_send_error' => 'Se pidió confirmación de correo electrónico pero el sistema no pudo enviar el correo electrónico. Contacte al administrador para asegurarse que el correo electrónico está configurado correctamente.',
|
||||||
'email_confirm_success' => '¡Tu correo electrónico ha sido confirmado! Ahora deberías poder iniciar sesión usando esta dirección de correo electrónico.',
|
'email_confirm_success' => '¡Su correo electrónico ha sido confirmado! Ahora debería poder iniciar sesión usando esta dirección de correo electrónico.',
|
||||||
'email_confirm_resent' => 'Correo electrónico de confirmación reenviado, Por favor verifique su bandeja de entrada.',
|
'email_confirm_resent' => 'Correo electrónico de confirmación reenviado, Por favor verifique su bandeja de entrada.',
|
||||||
|
|
||||||
'email_not_confirmed' => 'Dirección de correo electrónico no confirmada',
|
'email_not_confirmed' => 'Dirección de correo electrónico no confirmada',
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ return [
|
|||||||
'tags_remove' => 'Eliminar esta etiqueta',
|
'tags_remove' => 'Eliminar esta etiqueta',
|
||||||
'tags_usages' => 'Uso total de etiquetas',
|
'tags_usages' => 'Uso total de etiquetas',
|
||||||
'tags_assigned_pages' => 'Asignadas a páginas',
|
'tags_assigned_pages' => 'Asignadas a páginas',
|
||||||
'tags_assigned_chapters' => 'Asignadas a capitulos',
|
'tags_assigned_chapters' => 'Asignadas a capítulos',
|
||||||
'tags_assigned_books' => 'Asignadas a libros',
|
'tags_assigned_books' => 'Asignadas a libros',
|
||||||
'tags_assigned_shelves' => 'Asignadas a estantes',
|
'tags_assigned_shelves' => 'Asignadas a estantes',
|
||||||
'tags_x_unique_values' => ':count valores únicos',
|
'tags_x_unique_values' => ':count valores únicos',
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ 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_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_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',
|
'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_already_logged_in' => 'Ya está conectado',
|
||||||
'oidc_user_not_registered' => 'El usuario :name no está registrado y el registro automático está deshabilitado',
|
'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_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',
|
'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_no_action_defined' => 'Acción no definida',
|
||||||
'social_login_bad_response' => "SE recibió un Error durante el acceso con :socialAccount : \n:error",
|
'social_login_bad_response' => "SE recibió un Error durante el acceso con :socialAccount : \n:error",
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => 'Palun kinnita oma e-posti aadress, klõpsates alloleval nupul:',
|
'email_confirm_text' => 'Palun kinnita oma e-posti aadress, klõpsates alloleval nupul:',
|
||||||
'email_confirm_action' => 'Kinnita e-posti aadress',
|
'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_send_error' => 'E-posti aadressi kinnitamine on vajalik, aga e-kirja saatmine ebaõnnestus. Võta ühendust administraatoriga.',
|
||||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
'email_confirm_success' => 'E-posti aadress on kinnitatud! Nüüd saad selle aadressiga sisse logida.',
|
||||||
'email_confirm_resent' => 'Kinnituskiri on saadetud, vaata oma postkasti.',
|
'email_confirm_resent' => 'Kinnituskiri on saadetud, vaata oma postkasti.',
|
||||||
|
|
||||||
'email_not_confirmed' => 'E-posti aadress ei ole kinnitatud',
|
'email_not_confirmed' => 'E-posti aadress ei ole kinnitatud',
|
||||||
@@ -71,7 +71,7 @@ return [
|
|||||||
'user_invite_page_welcome' => 'Tere tulemast rakendusse :appName!',
|
'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_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_page_confirm_button' => 'Kinnita parool',
|
||||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
'user_invite_success_login' => 'Parool seatud, nüüd on sul selle parooli abil ligipääs rakendusele :appName!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Seadista mitmeastmeline autentimine',
|
'mfa_setup' => 'Seadista mitmeastmeline autentimine',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => 'Conferma il tuo indirizzo email cliccando il pulsante sotto:',
|
'email_confirm_text' => 'Conferma il tuo indirizzo email cliccando il pulsante sotto:',
|
||||||
'email_confirm_action' => 'Conferma Email',
|
'email_confirm_action' => 'Conferma Email',
|
||||||
'email_confirm_send_error' => 'La conferma della mail è richiesta ma non è stato possibile mandare la mail. Contatta l\'amministratore.',
|
'email_confirm_send_error' => 'La conferma della mail è richiesta ma non è stato possibile mandare la mail. Contatta l\'amministratore.',
|
||||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
'email_confirm_success' => 'La tua email è stata confermata! Ora dovresti essere in grado di effettuare il login utilizzando questo indirizzo email.',
|
||||||
'email_confirm_resent' => 'Mail di conferma reinviata, controlla la tua posta.',
|
'email_confirm_resent' => 'Mail di conferma reinviata, controlla la tua posta.',
|
||||||
|
|
||||||
'email_not_confirmed' => 'Indirizzo Email Non Confermato',
|
'email_not_confirmed' => 'Indirizzo Email Non Confermato',
|
||||||
@@ -71,7 +71,7 @@ return [
|
|||||||
'user_invite_page_welcome' => 'Benvenuto in :appName!',
|
'user_invite_page_welcome' => 'Benvenuto in :appName!',
|
||||||
'user_invite_page_text' => 'Per completare il tuo account e ottenere l\'accesso devi impostare una password che verrà utilizzata per accedere a :appName in futuro.',
|
'user_invite_page_text' => 'Per completare il tuo account e ottenere l\'accesso devi impostare una password che verrà utilizzata per accedere a :appName in futuro.',
|
||||||
'user_invite_page_confirm_button' => 'Conferma Password',
|
'user_invite_page_confirm_button' => 'Conferma Password',
|
||||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
'user_invite_success_login' => 'Password impostata, ora dovresti essere in grado di effettuare il login utilizzando la password impostata per accedere a :appName!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Imposta Autenticazione Multi-Fattore',
|
'mfa_setup' => 'Imposta Autenticazione Multi-Fattore',
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ return [
|
|||||||
'unfavourite' => 'Rimuovi dai preferiti',
|
'unfavourite' => 'Rimuovi dai preferiti',
|
||||||
'next' => 'Successivo',
|
'next' => 'Successivo',
|
||||||
'previous' => 'Precedente',
|
'previous' => 'Precedente',
|
||||||
'filter_active' => 'Active Filter:',
|
'filter_active' => 'Filtro attivo:',
|
||||||
'filter_clear' => 'Clear Filter',
|
'filter_clear' => 'Pulisci filtro',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => 'Opzioni Ordinamento',
|
'sort_options' => 'Opzioni Ordinamento',
|
||||||
|
|||||||
@@ -258,16 +258,16 @@ return [
|
|||||||
'tags_explain' => "Aggiungi tag per categorizzare meglio il contenuto. \n Puoi assegnare un valore ai tag per una migliore organizzazione.",
|
'tags_explain' => "Aggiungi tag per categorizzare meglio il contenuto. \n Puoi assegnare un valore ai tag per una migliore organizzazione.",
|
||||||
'tags_add' => 'Aggiungi un altro tag',
|
'tags_add' => 'Aggiungi un altro tag',
|
||||||
'tags_remove' => 'Rimuovi questo tag',
|
'tags_remove' => 'Rimuovi questo tag',
|
||||||
'tags_usages' => 'Total tag usages',
|
'tags_usages' => 'Utilizzo totale dei tag',
|
||||||
'tags_assigned_pages' => 'Assigned to Pages',
|
'tags_assigned_pages' => 'Assegnato alle Pagine',
|
||||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
'tags_assigned_chapters' => 'Assegnato ai capitoli',
|
||||||
'tags_assigned_books' => 'Assigned to Books',
|
'tags_assigned_books' => 'Assegnato a Libri',
|
||||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
'tags_assigned_shelves' => 'Assegnato alle Librerie',
|
||||||
'tags_x_unique_values' => ':count unique values',
|
'tags_x_unique_values' => ':count unique values',
|
||||||
'tags_all_values' => 'All values',
|
'tags_all_values' => 'Tutti i valori',
|
||||||
'tags_view_tags' => 'View Tags',
|
'tags_view_tags' => 'Visualizza tag',
|
||||||
'tags_view_existing_tags' => 'View existing tags',
|
'tags_view_existing_tags' => 'Usa i tag esistenti',
|
||||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
'tags_list_empty_hint' => 'I tag possono essere assegnati tramite la barra laterale dell\'editor di pagina o durante la modifica dei dettagli di un libro, capitolo o libreria.',
|
||||||
'attachments' => 'Allegati',
|
'attachments' => 'Allegati',
|
||||||
'attachments_explain' => 'Carica alcuni file o allega link per visualizzarli nella pagina. Questi sono visibili nella sidebar della pagina.',
|
'attachments_explain' => 'Carica alcuni file o allega link per visualizzarli nella pagina. Questi sono visibili nella sidebar della pagina.',
|
||||||
'attachments_explain_instant_save' => 'I cambiamenti qui sono salvati istantaneamente.',
|
'attachments_explain_instant_save' => 'I cambiamenti qui sono salvati istantaneamente.',
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ return [
|
|||||||
'unfavourite' => 'お気に入りから削除',
|
'unfavourite' => 'お気に入りから削除',
|
||||||
'next' => '次へ',
|
'next' => '次へ',
|
||||||
'previous' => '前へ',
|
'previous' => '前へ',
|
||||||
'filter_active' => 'Active Filter:',
|
'filter_active' => '有効なフィルター:',
|
||||||
'filter_clear' => 'Clear Filter',
|
'filter_clear' => 'フィルターを解除',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => '並べ替えオプション',
|
'sort_options' => '並べ替えオプション',
|
||||||
@@ -63,7 +63,7 @@ return [
|
|||||||
'no_activity' => '表示するアクティビティがありません',
|
'no_activity' => '表示するアクティビティがありません',
|
||||||
'no_items' => 'アイテムはありません',
|
'no_items' => 'アイテムはありません',
|
||||||
'back_to_top' => '上に戻る',
|
'back_to_top' => '上に戻る',
|
||||||
'skip_to_main_content' => 'Skip to main content',
|
'skip_to_main_content' => 'メインコンテンツへスキップ',
|
||||||
'toggle_details' => '概要の表示切替',
|
'toggle_details' => '概要の表示切替',
|
||||||
'toggle_thumbnails' => 'Toggle Thumbnails',
|
'toggle_thumbnails' => 'Toggle Thumbnails',
|
||||||
'details' => '詳細',
|
'details' => '詳細',
|
||||||
|
|||||||
@@ -75,35 +75,35 @@ return [
|
|||||||
// Shelves
|
// Shelves
|
||||||
'shelf' => '本棚',
|
'shelf' => '本棚',
|
||||||
'shelves' => '本棚',
|
'shelves' => '本棚',
|
||||||
'x_shelves' => ':count Shelf|:count Shelves',
|
'x_shelves' => ':count 本棚|:count 本棚',
|
||||||
'shelves_long' => '本棚',
|
'shelves_long' => '本棚',
|
||||||
'shelves_empty' => 'No shelves have been created',
|
'shelves_empty' => '本棚が作成されていません',
|
||||||
'shelves_create' => '新しい本棚を作成',
|
'shelves_create' => '新しい本棚を作成',
|
||||||
'shelves_popular' => '人気の本棚',
|
'shelves_popular' => '人気の本棚',
|
||||||
'shelves_new' => '新しい本棚',
|
'shelves_new' => '新しい本棚',
|
||||||
'shelves_new_action' => '新しい本棚',
|
'shelves_new_action' => '新しい本棚',
|
||||||
'shelves_popular_empty' => 'The most popular shelves will appear here.',
|
'shelves_popular_empty' => 'ここに人気の本棚が表示されます。',
|
||||||
'shelves_new_empty' => 'The most recently created shelves will appear here.',
|
'shelves_new_empty' => '最近作成された本棚がここに表示されます。',
|
||||||
'shelves_save' => '本棚を保存',
|
'shelves_save' => '本棚を保存',
|
||||||
'shelves_books' => 'この本棚のブック',
|
'shelves_books' => 'この本棚のブック',
|
||||||
'shelves_add_books' => 'この本棚にブックを追加',
|
'shelves_add_books' => 'この本棚にブックを追加',
|
||||||
'shelves_drag_books' => 'ブックをここにドラッグすると本棚に追加されます',
|
'shelves_drag_books' => 'ブックをここにドラッグすると本棚に追加されます',
|
||||||
'shelves_empty_contents' => 'This shelf has no books assigned to it',
|
'shelves_empty_contents' => 'この本棚にはブックが割り当てられていません。',
|
||||||
'shelves_edit_and_assign' => 'Edit shelf to assign books',
|
'shelves_edit_and_assign' => '本棚を編集してブックを割り当てる',
|
||||||
'shelves_edit_named' => '本棚「:name」を編集',
|
'shelves_edit_named' => '本棚「:name」を編集',
|
||||||
'shelves_edit' => '本棚を編集',
|
'shelves_edit' => '本棚を編集',
|
||||||
'shelves_delete' => '本棚を削除',
|
'shelves_delete' => '本棚を削除',
|
||||||
'shelves_delete_named' => '本棚「:name」を削除',
|
'shelves_delete_named' => '本棚「:name」を削除',
|
||||||
'shelves_delete_explain' => "これにより、この本棚「:name」が削除されます。含まれているブックは削除されません。",
|
'shelves_delete_explain' => "これにより、この本棚「:name」が削除されます。含まれているブックは削除されません。",
|
||||||
'shelves_delete_confirmation' => '本当にこの本棚を削除してよろしいですか?',
|
'shelves_delete_confirmation' => '本当にこの本棚を削除してよろしいですか?',
|
||||||
'shelves_permissions' => 'Bookshelf Permissions',
|
'shelves_permissions' => '本棚の権限',
|
||||||
'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
|
'shelves_permissions_updated' => '本棚の権限を更新しました',
|
||||||
'shelves_permissions_active' => 'Bookshelf Permissions Active',
|
'shelves_permissions_active' => '本棚の権限は有効です',
|
||||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
'shelves_permissions_cascade_warning' => '本棚の権限は含まれる本には自動的に継承されません。これは、1つのブックが複数の本棚に存在する可能性があるためです。ただし、以下のオプションを使用すると権限を子ブックにコピーできます。',
|
||||||
'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
|
'shelves_copy_permissions_to_books' => 'ブックに権限をコピー',
|
||||||
'shelves_copy_permissions' => 'Copy Permissions',
|
'shelves_copy_permissions' => '権限をコピー',
|
||||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
|
'shelves_copy_permissions_explain' => 'これにより、この本棚の現在の権限設定を本棚に含まれるすべてのブックに適用します。有効にする前に、この本棚の権限への変更が保存されていることを確認してください。',
|
||||||
'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
|
'shelves_copy_permission_success' => '本棚の権限が:count個のブックにコピーされました',
|
||||||
|
|
||||||
// Books
|
// Books
|
||||||
'book' => 'ブック',
|
'book' => 'ブック',
|
||||||
@@ -115,7 +115,7 @@ return [
|
|||||||
'books_new' => '新しいブック',
|
'books_new' => '新しいブック',
|
||||||
'books_new_action' => '新しいブック',
|
'books_new_action' => '新しいブック',
|
||||||
'books_popular_empty' => 'ここに人気のブックが表示されます。',
|
'books_popular_empty' => 'ここに人気のブックが表示されます。',
|
||||||
'books_new_empty' => 'The most recently created books will appear here.',
|
'books_new_empty' => '最近作成されたブックがここに表示されます。',
|
||||||
'books_create' => '新しいブックを作成',
|
'books_create' => '新しいブックを作成',
|
||||||
'books_delete' => 'ブックを削除',
|
'books_delete' => 'ブックを削除',
|
||||||
'books_delete_named' => 'ブック「:bookName」を削除',
|
'books_delete_named' => 'ブック「:bookName」を削除',
|
||||||
@@ -153,7 +153,7 @@ return [
|
|||||||
'chapters_create' => 'チャプターを作成',
|
'chapters_create' => 'チャプターを作成',
|
||||||
'chapters_delete' => 'チャプターを削除',
|
'chapters_delete' => 'チャプターを削除',
|
||||||
'chapters_delete_named' => 'チャプター「:chapterName」を削除',
|
'chapters_delete_named' => 'チャプター「:chapterName」を削除',
|
||||||
'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
|
'chapters_delete_explain' => 'これにより、チャプター「:chapterName」が削除されます。このチャプターに存在するページもすべて削除されます。',
|
||||||
'chapters_delete_confirm' => 'チャプターを削除してよろしいですか?',
|
'chapters_delete_confirm' => 'チャプターを削除してよろしいですか?',
|
||||||
'chapters_edit' => 'チャプターを編集',
|
'chapters_edit' => 'チャプターを編集',
|
||||||
'chapters_edit_named' => 'チャプター「:chapterName」を編集',
|
'chapters_edit_named' => 'チャプター「:chapterName」を編集',
|
||||||
@@ -234,7 +234,7 @@ return [
|
|||||||
'pages_initial_name' => '新規ページ',
|
'pages_initial_name' => '新規ページ',
|
||||||
'pages_editing_draft_notification' => ':timeDiffに保存された下書きを編集しています。',
|
'pages_editing_draft_notification' => ':timeDiffに保存された下書きを編集しています。',
|
||||||
'pages_draft_edited_notification' => 'このページは更新されています。下書きを破棄することを推奨します。',
|
'pages_draft_edited_notification' => 'このページは更新されています。下書きを破棄することを推奨します。',
|
||||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
'pages_draft_page_changed_since_creation' => 'この下書きが作成されてから、このページが更新されました。この下書きを破棄するか、ページの変更を上書きしないように注意することを推奨します。',
|
||||||
'pages_draft_edit_active' => [
|
'pages_draft_edit_active' => [
|
||||||
'start_a' => ':count人のユーザがページの編集を開始しました',
|
'start_a' => ':count人のユーザがページの編集を開始しました',
|
||||||
'start_b' => ':userNameがページの編集を開始しました',
|
'start_b' => ':userNameがページの編集を開始しました',
|
||||||
@@ -244,7 +244,7 @@ return [
|
|||||||
],
|
],
|
||||||
'pages_draft_discarded' => '下書きが破棄されました。エディタは現在の内容へ復元されています。',
|
'pages_draft_discarded' => '下書きが破棄されました。エディタは現在の内容へ復元されています。',
|
||||||
'pages_specific' => '特定のページ',
|
'pages_specific' => '特定のページ',
|
||||||
'pages_is_template' => 'Page Template',
|
'pages_is_template' => 'ページテンプレート',
|
||||||
|
|
||||||
// Editor Sidebar
|
// Editor Sidebar
|
||||||
'page_tags' => 'タグ',
|
'page_tags' => 'タグ',
|
||||||
@@ -258,16 +258,16 @@ return [
|
|||||||
'tags_explain' => "タグを設定すると、コンテンツの管理が容易になります。\nより高度な管理をしたい場合、タグに内容を設定できます。",
|
'tags_explain' => "タグを設定すると、コンテンツの管理が容易になります。\nより高度な管理をしたい場合、タグに内容を設定できます。",
|
||||||
'tags_add' => 'タグを追加',
|
'tags_add' => 'タグを追加',
|
||||||
'tags_remove' => 'このタグを削除',
|
'tags_remove' => 'このタグを削除',
|
||||||
'tags_usages' => 'Total tag usages',
|
'tags_usages' => 'タグの総使用回数',
|
||||||
'tags_assigned_pages' => 'Assigned to Pages',
|
'tags_assigned_pages' => '割り当てられているページの数',
|
||||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
'tags_assigned_chapters' => '割り当てられているチャプターの数',
|
||||||
'tags_assigned_books' => 'Assigned to Books',
|
'tags_assigned_books' => '割り当てられているブックの数',
|
||||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
'tags_assigned_shelves' => '割り当てられている本棚の数',
|
||||||
'tags_x_unique_values' => ':count unique values',
|
'tags_x_unique_values' => ':count個のユニークな値',
|
||||||
'tags_all_values' => 'All values',
|
'tags_all_values' => '全ての値',
|
||||||
'tags_view_tags' => 'View Tags',
|
'tags_view_tags' => 'タグを表示',
|
||||||
'tags_view_existing_tags' => 'View existing tags',
|
'tags_view_existing_tags' => '既存のタグを表示',
|
||||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
'tags_list_empty_hint' => 'タグはページエディタのサイドバーまたはブック、チャプター、本棚の詳細を編集しているときに割り当てることができます。',
|
||||||
'attachments' => '添付ファイル',
|
'attachments' => '添付ファイル',
|
||||||
'attachments_explain' => 'ファイルをアップロードまたはリンクを添付することができます。これらはサイドバーで確認できます。',
|
'attachments_explain' => 'ファイルをアップロードまたはリンクを添付することができます。これらはサイドバーで確認できます。',
|
||||||
'attachments_explain_instant_save' => 'この変更は即座に保存されます。',
|
'attachments_explain_instant_save' => 'この変更は即座に保存されます。',
|
||||||
@@ -275,7 +275,7 @@ return [
|
|||||||
'attachments_upload' => 'アップロード',
|
'attachments_upload' => 'アップロード',
|
||||||
'attachments_link' => 'リンクを添付',
|
'attachments_link' => 'リンクを添付',
|
||||||
'attachments_set_link' => 'リンクを設定',
|
'attachments_set_link' => 'リンクを設定',
|
||||||
'attachments_delete' => 'Are you sure you want to delete this attachment?',
|
'attachments_delete' => 'この添付ファイルを削除してよろしいですか?',
|
||||||
'attachments_dropzone' => 'ファイルをドロップするか、クリックして選択',
|
'attachments_dropzone' => 'ファイルをドロップするか、クリックして選択',
|
||||||
'attachments_no_files' => 'ファイルはアップロードされていません',
|
'attachments_no_files' => 'ファイルはアップロードされていません',
|
||||||
'attachments_explain_link' => 'ファイルをアップロードしたくない場合、他のページやクラウド上のファイルへのリンクを添付できます。',
|
'attachments_explain_link' => 'ファイルをアップロードしたくない場合、他のページやクラウド上のファイルへのリンクを添付できます。',
|
||||||
@@ -284,7 +284,7 @@ return [
|
|||||||
'attachments_link_url' => 'ファイルURL',
|
'attachments_link_url' => 'ファイルURL',
|
||||||
'attachments_link_url_hint' => 'WebサイトまたはファイルへのURL',
|
'attachments_link_url_hint' => 'WebサイトまたはファイルへのURL',
|
||||||
'attach' => '添付',
|
'attach' => '添付',
|
||||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
'attachments_insert_link' => '添付ファイルへのリンクをページに追加',
|
||||||
'attachments_edit_file' => 'ファイルを編集',
|
'attachments_edit_file' => 'ファイルを編集',
|
||||||
'attachments_edit_file_name' => 'ファイル名',
|
'attachments_edit_file_name' => 'ファイル名',
|
||||||
'attachments_edit_drop_upload' => 'ファイルをドロップするか、クリックしてアップロード',
|
'attachments_edit_drop_upload' => 'ファイルをドロップするか、クリックしてアップロード',
|
||||||
@@ -294,12 +294,12 @@ return [
|
|||||||
'attachments_file_uploaded' => 'ファイルがアップロードされました',
|
'attachments_file_uploaded' => 'ファイルがアップロードされました',
|
||||||
'attachments_file_updated' => 'ファイルが更新されました',
|
'attachments_file_updated' => 'ファイルが更新されました',
|
||||||
'attachments_link_attached' => 'リンクがページへ添付されました',
|
'attachments_link_attached' => 'リンクがページへ添付されました',
|
||||||
'templates' => 'Templates',
|
'templates' => 'テンプレート',
|
||||||
'templates_set_as_template' => 'Page is a template',
|
'templates_set_as_template' => 'テンプレートに設定',
|
||||||
'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',
|
'templates_explain_set_as_template' => 'このページをテンプレートとして設定すると、他のページを作成する際にこの内容を利用することができます。他のユーザーは、このページの表示権限を持っていればこのテンプレートを使用できます。',
|
||||||
'templates_replace_content' => 'Replace page content',
|
'templates_replace_content' => 'ページの内容を置換',
|
||||||
'templates_append_content' => 'Append to page content',
|
'templates_append_content' => 'ページの末尾に追加',
|
||||||
'templates_prepend_content' => 'Prepend to page content',
|
'templates_prepend_content' => 'ページの先頭に追加',
|
||||||
|
|
||||||
// Profile View
|
// Profile View
|
||||||
'profile_user_for_x' => ':time前に作成',
|
'profile_user_for_x' => ':time前に作成',
|
||||||
@@ -307,7 +307,7 @@ return [
|
|||||||
'profile_not_created_pages' => ':userNameはページを作成していません',
|
'profile_not_created_pages' => ':userNameはページを作成していません',
|
||||||
'profile_not_created_chapters' => ':userNameはチャプターを作成していません',
|
'profile_not_created_chapters' => ':userNameはチャプターを作成していません',
|
||||||
'profile_not_created_books' => ':userNameはブックを作成していません',
|
'profile_not_created_books' => ':userNameはブックを作成していません',
|
||||||
'profile_not_created_shelves' => ':userName has not created any shelves',
|
'profile_not_created_shelves' => ':userNameは本棚を作成していません',
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
'comment' => 'コメント',
|
'comment' => 'コメント',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ return [
|
|||||||
'name' => 'Navn',
|
'name' => 'Navn',
|
||||||
'description' => 'Beskrivelse',
|
'description' => 'Beskrivelse',
|
||||||
'role' => 'Rolle',
|
'role' => 'Rolle',
|
||||||
'cover_image' => 'Bokomslag',
|
'cover_image' => 'Forside',
|
||||||
'cover_image_description' => 'Bildet bør være ca. 440x250px.',
|
'cover_image_description' => 'Bildet bør være ca. 440x250px.',
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ return [
|
|||||||
'images' => 'Bilder',
|
'images' => 'Bilder',
|
||||||
'my_recent_drafts' => 'Mine nylige utkast',
|
'my_recent_drafts' => 'Mine nylige utkast',
|
||||||
'my_recently_viewed' => 'Mine nylige visninger',
|
'my_recently_viewed' => 'Mine nylige visninger',
|
||||||
'my_most_viewed_favourites' => 'Mine mest viste favoritter',
|
'my_most_viewed_favourites' => 'Mine mest sette favoritter',
|
||||||
'my_favourites' => 'Mine favoritter',
|
'my_favourites' => 'Mine favoritter',
|
||||||
'no_pages_viewed' => 'Du har ikke sett på noen sider',
|
'no_pages_viewed' => 'Du har ikke sett på noen sider',
|
||||||
'no_pages_recently_created' => 'Ingen sider har nylig blitt opprettet',
|
'no_pages_recently_created' => 'Ingen sider har nylig blitt opprettet',
|
||||||
@@ -99,7 +99,7 @@ return [
|
|||||||
'shelves_permissions' => 'Tilganger til hylla',
|
'shelves_permissions' => 'Tilganger til hylla',
|
||||||
'shelves_permissions_updated' => 'Hyllas tilganger er oppdatert',
|
'shelves_permissions_updated' => 'Hyllas tilganger er oppdatert',
|
||||||
'shelves_permissions_active' => 'Hyllas tilganger er aktive',
|
'shelves_permissions_active' => 'Hyllas tilganger er aktive',
|
||||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
'shelves_permissions_cascade_warning' => 'Tillatelser på bokhyller vil ikke automatisk fordeles til bøkene på aktuell bokhylle. Dette da en bok kan tilhøre flere bokhyller. Tillatelser kan imidlertid kopieres til underliggende bøker ved å benytte alternativet nedenfor.',
|
||||||
'shelves_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla',
|
'shelves_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla',
|
||||||
'shelves_copy_permissions' => 'Kopier tilganger',
|
'shelves_copy_permissions' => 'Kopier tilganger',
|
||||||
'shelves_copy_permissions_explain' => 'Dette vil angi gjeldende tillatelsesinnstillinger for denne bokhyllen på alle bøkene som finnes på den. Før du aktiverer, må du forsikre deg om at endringer i tillatelsene til denne bokhyllen er lagret.',
|
'shelves_copy_permissions_explain' => 'Dette vil angi gjeldende tillatelsesinnstillinger for denne bokhyllen på alle bøkene som finnes på den. Før du aktiverer, må du forsikre deg om at endringer i tillatelsene til denne bokhyllen er lagret.',
|
||||||
@@ -234,7 +234,7 @@ return [
|
|||||||
'pages_initial_name' => 'Ny side',
|
'pages_initial_name' => 'Ny side',
|
||||||
'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',
|
'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',
|
||||||
'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',
|
'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',
|
||||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
'pages_draft_page_changed_since_creation' => 'Denne siden er blitt oppdatert etter at dette utkastet ble opprettet. Det anbefales at du forkaster dette utkastet, eller er ekstra forsiktig slik at du ikke overskriver noen sideendringer.',
|
||||||
'pages_draft_edit_active' => [
|
'pages_draft_edit_active' => [
|
||||||
'start_a' => ':count forfattere har begynt å endre denne siden.',
|
'start_a' => ':count forfattere har begynt å endre denne siden.',
|
||||||
'start_b' => ':userName skriver på siden for øyeblikket',
|
'start_b' => ':userName skriver på siden for øyeblikket',
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'failed' => 'As credenciais fornecidas não correspondem aos nossos registos.',
|
'failed' => 'Estas credenciais não coincidem com os nossos registos.',
|
||||||
'throttle' => 'Demasiadas tentativas de início de sessão. Por favor, tente novamente em :seconds segundos.',
|
'throttle' => 'Demasiadas tentativas de acesso. Tente novamente em :seconds segundos.',
|
||||||
|
|
||||||
// Login & Register
|
// Login & Register
|
||||||
'sign_up' => 'Criar conta',
|
'sign_up' => 'Registar',
|
||||||
'log_in' => 'Iniciar sessão',
|
'log_in' => 'Iniciar sessão',
|
||||||
'log_in_with' => 'Iniciar sessão com :socialDriver',
|
'log_in_with' => 'Iniciar sessão com :socialDriver',
|
||||||
'sign_up_with' => 'Criar conta com :socialDriver',
|
'sign_up_with' => 'Criar conta com :socialDriver',
|
||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => 'Por favor, confirme o seu endereço de e-mail ao carregar no botão abaixo:',
|
'email_confirm_text' => 'Por favor, confirme o seu endereço de e-mail ao carregar no botão abaixo:',
|
||||||
'email_confirm_action' => 'Confirmar E-mail',
|
'email_confirm_action' => 'Confirmar E-mail',
|
||||||
'email_confirm_send_error' => 'A confirmação do endereço de e-mail é requerida, mas o sistema não pôde enviar a mensagem. Por favor, entre em contacto com o administrador para se certificar que o serviço de envio de e-mails está corretamente configurado.',
|
'email_confirm_send_error' => 'A confirmação do endereço de e-mail é requerida, mas o sistema não pôde enviar a mensagem. Por favor, entre em contacto com o administrador para se certificar que o serviço de envio de e-mails está corretamente configurado.',
|
||||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
'email_confirm_success' => 'O seu endereço de email foi confirmado! Neste momento já poderá entrar usando este endereço de email.',
|
||||||
'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, verifique a sua caixa de entrada.',
|
'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, verifique a sua caixa de entrada.',
|
||||||
|
|
||||||
'email_not_confirmed' => 'Endereço de E-mail Não Confirmado',
|
'email_not_confirmed' => 'Endereço de E-mail Não Confirmado',
|
||||||
@@ -71,7 +71,7 @@ return [
|
|||||||
'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!',
|
'user_invite_page_welcome' => 'Bem-vindo(a) a :appName!',
|
||||||
'user_invite_page_text' => 'Para finalizar a sua conta e obter acesso, precisa de definir uma senha que será utilizada para efetuar login em :appName em visitas futuras.',
|
'user_invite_page_text' => 'Para finalizar a sua conta e obter acesso, precisa de definir uma senha que será utilizada para efetuar login em :appName em visitas futuras.',
|
||||||
'user_invite_page_confirm_button' => 'Confirmar Palavra-Passe',
|
'user_invite_page_confirm_button' => 'Confirmar Palavra-Passe',
|
||||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
'user_invite_success_login' => 'Palavra passe definida, agora poderá entrar usado a sua nova palavra passe para acessar :appName!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Configurar autenticação de múltiplos fatores',
|
'mfa_setup' => 'Configurar autenticação de múltiplos fatores',
|
||||||
@@ -80,31 +80,31 @@ return [
|
|||||||
'mfa_setup_reconfigure' => 'Reconfigurar',
|
'mfa_setup_reconfigure' => 'Reconfigurar',
|
||||||
'mfa_setup_remove_confirmation' => 'Tem a certeza que deseja remover este método de autenticação de múltiplos fatores?',
|
'mfa_setup_remove_confirmation' => 'Tem a certeza que deseja remover este método de autenticação de múltiplos fatores?',
|
||||||
'mfa_setup_action' => 'Configuração',
|
'mfa_setup_action' => 'Configuração',
|
||||||
'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_backup_codes_usage_limit_warning' => 'Você tem menos de 5 códigos de backup restantes, Por favor, gere e armazene um novo conjunto antes de esgotar os códigos para evitar estar bloqueado para fora da sua conta.',
|
||||||
'mfa_option_totp_title' => 'Mobile App',
|
'mfa_option_totp_title' => 'Aplicação móvel',
|
||||||
'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_totp_desc' => 'Para usar a autenticação multi-fator, você precisará de um aplicativo móvel que suporte TOTP como o Autenticador do Google, Authy ou o autenticador Microsoft.',
|
||||||
'mfa_option_backup_codes_title' => 'Backup Codes',
|
'mfa_option_backup_codes_title' => 'Códigos de Backup',
|
||||||
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
|
'mfa_option_backup_codes_desc' => 'Armazene com segurança um conjunto de códigos de backup únicos que você pode inserir para verificar sua identidade.',
|
||||||
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
|
'mfa_gen_confirm_and_enable' => 'Confirmar e ativar',
|
||||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
'mfa_gen_backup_codes_title' => 'Configuração dos Códigos de Backup',
|
||||||
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
|
'mfa_gen_backup_codes_desc' => 'Armazene a lista de códigos abaixo em um lugar seguro. Ao acessar o sistema você poderá usar um dos códigos como um segundo mecanismo de autenticação.',
|
||||||
'mfa_gen_backup_codes_download' => 'Download Codes',
|
'mfa_gen_backup_codes_download' => 'Transferir códigos',
|
||||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
'mfa_gen_backup_codes_usage_warning' => 'Cada código só pode ser usado uma vez',
|
||||||
'mfa_gen_totp_title' => 'Mobile App Setup',
|
'mfa_gen_totp_title' => 'Configuração de aplicativo móvel',
|
||||||
'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_desc' => 'Para usar a autenticação multi-fator, você precisará de um aplicativo móvel que suporte TOTP como o Autenticador do Google, Authy ou o autenticador Microsoft.',
|
||||||
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
|
'mfa_gen_totp_scan' => 'Leia o código QR abaixo usando seu aplicativo de autenticação preferido para começar.',
|
||||||
'mfa_gen_totp_verify_setup' => 'Verify Setup',
|
'mfa_gen_totp_verify_setup' => 'Verificar configuração',
|
||||||
'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_verify_setup_desc' => 'Verifique se tudo está funcionando digitando um código, gerado dentro do seu aplicativo de autenticação, na caixa de entrada abaixo:',
|
||||||
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
'mfa_gen_totp_provide_code_here' => 'Forneça o código gerado pelo aplicativo aqui',
|
||||||
'mfa_verify_access' => 'Verify Access',
|
'mfa_verify_access' => 'Verificar Acesso',
|
||||||
'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_access_desc' => 'Sua conta de usuário requer que você confirme sua identidade por meio de um nível adicional de verificação antes de conceder o acesso. Verifique o uso de um dos métodos configurados para continuar.',
|
||||||
'mfa_verify_no_methods' => 'No Methods Configured',
|
'mfa_verify_no_methods' => 'Nenhum método configurado',
|
||||||
'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_no_methods_desc' => 'Nenhum método de autenticação de vários fatores foi encontrado para a sua conta. Você precisará configurar pelo menos um método antes de ganhar acesso.',
|
||||||
'mfa_verify_use_totp' => 'Verify using a mobile app',
|
'mfa_verify_use_totp' => 'Verificar usando um aplicativo móvel',
|
||||||
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
|
'mfa_verify_use_backup_codes' => 'Verificar usando código de backup',
|
||||||
'mfa_verify_backup_code' => 'Backup Code',
|
'mfa_verify_backup_code' => 'Código de backup',
|
||||||
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
|
'mfa_verify_backup_code_desc' => 'Insira um dos seus códigos de backup restantes abaixo:',
|
||||||
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
|
'mfa_verify_backup_code_enter_here' => 'Insira o código de backup aqui',
|
||||||
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
|
'mfa_verify_totp_desc' => 'Digite o código, gerado através do seu aplicativo móvel, abaixo:',
|
||||||
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
|
'mfa_setup_login_notification' => 'Método de multi-fatores configurado, por favor faça login novamente usando o método configurado.',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ return [
|
|||||||
'unfavourite' => 'Retirar Favorito',
|
'unfavourite' => 'Retirar Favorito',
|
||||||
'next' => 'Próximo',
|
'next' => 'Próximo',
|
||||||
'previous' => 'Anterior',
|
'previous' => 'Anterior',
|
||||||
'filter_active' => 'Active Filter:',
|
'filter_active' => 'Filtro Ativo:',
|
||||||
'filter_clear' => 'Clear Filter',
|
'filter_clear' => 'Limpar Filtro',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => 'Opções de Ordenação',
|
'sort_options' => 'Opções de Ordenação',
|
||||||
|
|||||||
@@ -258,15 +258,15 @@ return [
|
|||||||
'tags_explain' => "Adicione algumas etiquetas para melhor categorizar o seu conteúdo. \n Você poderá atribuir valores às etiquetas para uma organização mais complexa.",
|
'tags_explain' => "Adicione algumas etiquetas para melhor categorizar o seu conteúdo. \n Você poderá atribuir valores às etiquetas para uma organização mais complexa.",
|
||||||
'tags_add' => 'Adicionar outra etiqueta',
|
'tags_add' => 'Adicionar outra etiqueta',
|
||||||
'tags_remove' => 'Remover esta etiqueta',
|
'tags_remove' => 'Remover esta etiqueta',
|
||||||
'tags_usages' => 'Total tag usages',
|
'tags_usages' => 'Total de marcadores usados',
|
||||||
'tags_assigned_pages' => 'Assigned to Pages',
|
'tags_assigned_pages' => 'Atribuído às páginas',
|
||||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
'tags_assigned_chapters' => 'Atribuído aos Capítulos',
|
||||||
'tags_assigned_books' => 'Assigned to Books',
|
'tags_assigned_books' => 'Atribuído a Livros',
|
||||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
'tags_assigned_shelves' => 'Atribuído a Prateleiras',
|
||||||
'tags_x_unique_values' => ':count unique values',
|
'tags_x_unique_values' => ':count valores únicos',
|
||||||
'tags_all_values' => 'All values',
|
'tags_all_values' => 'Todos os valores',
|
||||||
'tags_view_tags' => 'View Tags',
|
'tags_view_tags' => 'Ver Marcadores',
|
||||||
'tags_view_existing_tags' => 'View existing tags',
|
'tags_view_existing_tags' => 'Ver marcadores existentes',
|
||||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
||||||
'attachments' => 'Anexos',
|
'attachments' => 'Anexos',
|
||||||
'attachments_explain' => 'Carregue alguns arquivos ou anexe links para serem exibidos na sua página. Eles estarão visíveis na barra lateral à direita.',
|
'attachments_explain' => 'Carregue alguns arquivos ou anexe links para serem exibidos na sua página. Eles estarão visíveis na barra lateral à direita.',
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ return [
|
|||||||
'app_homepage_desc' => 'Selecione uma opção para ser exibida como página inicial em vez da padrão. Permissões de página serão ignoradas para as páginas selecionadas.',
|
'app_homepage_desc' => 'Selecione uma opção para ser exibida como página inicial em vez da padrão. Permissões de página serão ignoradas para as páginas selecionadas.',
|
||||||
'app_homepage_select' => 'Selecione uma página',
|
'app_homepage_select' => 'Selecione uma página',
|
||||||
'app_footer_links' => 'Links do Rodapé',
|
'app_footer_links' => 'Links do Rodapé',
|
||||||
'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
|
'app_footer_links_desc' => 'Adicionar links para mostrar dentro do rodapé do site. Estes serão exibidos na parte inferior da maioria das páginas, incluindo aqueles que não necessitam de login. Você pode usar uma etiqueta de "trans::<key>" para usar traduções definidas pelo sistema. Por exemplo: Usando "trans::common.privacy_policy" fornecerá o texto traduzido "Política de Privacidade" e "trans::common.terms_of_service" fornecerá o texto traduzido "Termos de Serviço".',
|
||||||
'app_footer_links_label' => 'Etiqueta do Link',
|
'app_footer_links_label' => 'Etiqueta do Link',
|
||||||
'app_footer_links_url' => 'URL do Link',
|
'app_footer_links_url' => 'URL do Link',
|
||||||
'app_footer_links_add' => 'Adicionar Link de Rodapé',
|
'app_footer_links_add' => 'Adicionar Link de Rodapé',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ return [
|
|||||||
'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e underlines.',
|
'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e underlines.',
|
||||||
'alpha_num' => 'O campo :attribute deve conter apenas letras e números.',
|
'alpha_num' => 'O campo :attribute deve conter apenas letras e números.',
|
||||||
'array' => 'O campo :attribute deve ser uma array.',
|
'array' => 'O campo :attribute deve ser uma array.',
|
||||||
'backup_codes' => 'The provided code is not valid or has already been used.',
|
'backup_codes' => 'O código fornecido não é válido ou já foi usado.',
|
||||||
'before' => 'O campo :attribute deve ser uma data anterior à data :date.',
|
'before' => 'O campo :attribute deve ser uma data anterior à data :date.',
|
||||||
'between' => [
|
'between' => [
|
||||||
'numeric' => 'O campo :attribute deve estar entre :min e :max.',
|
'numeric' => 'O campo :attribute deve estar entre :min e :max.',
|
||||||
@@ -99,7 +99,7 @@ return [
|
|||||||
],
|
],
|
||||||
'string' => 'O campo :attribute deve ser uma string.',
|
'string' => 'O campo :attribute deve ser uma string.',
|
||||||
'timezone' => 'O campo :attribute deve conter uma timezone válida.',
|
'timezone' => 'O campo :attribute deve conter uma timezone válida.',
|
||||||
'totp' => 'The provided code is not valid or has expired.',
|
'totp' => 'O código fornecido não é válido ou expirou.',
|
||||||
'unique' => 'Já existe um campo/dado de nome :attribute.',
|
'unique' => 'Já existe um campo/dado de nome :attribute.',
|
||||||
'url' => 'O formato da URL :attribute é inválido.',
|
'url' => 'O formato da URL :attribute é inválido.',
|
||||||
'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',
|
'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => 'Пожалуйста, подтвердите свой адрес электронной почты нажав на кнопку ниже:',
|
'email_confirm_text' => 'Пожалуйста, подтвердите свой адрес электронной почты нажав на кнопку ниже:',
|
||||||
'email_confirm_action' => 'Подтвердить адрес электронной почты',
|
'email_confirm_action' => 'Подтвердить адрес электронной почты',
|
||||||
'email_confirm_send_error' => 'Требуется подтверждение электронной почты, но система не может отправить письмо. Свяжитесь с администратором, чтобы убедиться, что адрес электронной почты настроен правильно.',
|
'email_confirm_send_error' => 'Требуется подтверждение электронной почты, но система не может отправить письмо. Свяжитесь с администратором, чтобы убедиться, что адрес электронной почты настроен правильно.',
|
||||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
'email_confirm_success' => 'Ваш адрес электронной почты был подтвержден! Теперь вы можете войти в систему, используя этот адрес электронной почты.',
|
||||||
'email_confirm_resent' => 'Письмо с подтверждение выслано снова. Пожалуйста, проверьте ваш почтовый ящик.',
|
'email_confirm_resent' => 'Письмо с подтверждение выслано снова. Пожалуйста, проверьте ваш почтовый ящик.',
|
||||||
|
|
||||||
'email_not_confirmed' => 'Адрес электронной почты не подтвержден',
|
'email_not_confirmed' => 'Адрес электронной почты не подтвержден',
|
||||||
@@ -71,7 +71,7 @@ return [
|
|||||||
'user_invite_page_welcome' => 'Добро пожаловать в :appName!',
|
'user_invite_page_welcome' => 'Добро пожаловать в :appName!',
|
||||||
'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.',
|
'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.',
|
||||||
'user_invite_page_confirm_button' => 'Подтвердите пароль',
|
'user_invite_page_confirm_button' => 'Подтвердите пароль',
|
||||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
'user_invite_success_login' => 'Пароль установлен, теперь вы можете войти в систему, используя установленный пароль для доступа к :appName!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Двухфакторная аутентификация',
|
'mfa_setup' => 'Двухфакторная аутентификация',
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ return [
|
|||||||
'reset' => 'Сбросить',
|
'reset' => 'Сбросить',
|
||||||
'remove' => 'Удалить',
|
'remove' => 'Удалить',
|
||||||
'add' => 'Добавить',
|
'add' => 'Добавить',
|
||||||
'configure' => 'Configure',
|
'configure' => 'Настройка',
|
||||||
'fullscreen' => 'На весь экран',
|
'fullscreen' => 'На весь экран',
|
||||||
'favourite' => 'Избранное',
|
'favourite' => 'Избранное',
|
||||||
'unfavourite' => 'Убрать из избранного',
|
'unfavourite' => 'Убрать из избранного',
|
||||||
'next' => 'Следующая',
|
'next' => 'Следующая',
|
||||||
'previous' => 'Предыдущая',
|
'previous' => 'Предыдущая',
|
||||||
'filter_active' => 'Active Filter:',
|
'filter_active' => 'Активный фильтр:',
|
||||||
'filter_clear' => 'Clear Filter',
|
'filter_clear' => 'Сбросить фильтр',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => 'Параметры сортировки',
|
'sort_options' => 'Параметры сортировки',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ return [
|
|||||||
'export_html' => 'Веб файл',
|
'export_html' => 'Веб файл',
|
||||||
'export_pdf' => 'PDF файл',
|
'export_pdf' => 'PDF файл',
|
||||||
'export_text' => 'Текстовый файл',
|
'export_text' => 'Текстовый файл',
|
||||||
'export_md' => 'Markdown File',
|
'export_md' => 'Файл Markdown',
|
||||||
|
|
||||||
// Permissions and restrictions
|
// Permissions and restrictions
|
||||||
'permissions' => 'Разрешения',
|
'permissions' => 'Разрешения',
|
||||||
@@ -99,7 +99,7 @@ return [
|
|||||||
'shelves_permissions' => 'Доступы к книжной полке',
|
'shelves_permissions' => 'Доступы к книжной полке',
|
||||||
'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
|
'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
|
||||||
'shelves_permissions_active' => 'Действующие разрешения книжной полки',
|
'shelves_permissions_active' => 'Действующие разрешения книжной полки',
|
||||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
'shelves_permissions_cascade_warning' => 'Разрешения на полки не наследуются автоматически содержащимся в них книгам. Это происходит потому, что книга может находиться на нескольких полках. Однако разрешения могут быть установлены для книг полки с помощью опции, приведенной ниже.',
|
||||||
'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
|
'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
|
||||||
'shelves_copy_permissions' => 'Копировать доступы',
|
'shelves_copy_permissions' => 'Копировать доступы',
|
||||||
'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
|
'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
|
||||||
@@ -234,7 +234,7 @@ return [
|
|||||||
'pages_initial_name' => 'Новая страница',
|
'pages_initial_name' => 'Новая страница',
|
||||||
'pages_editing_draft_notification' => 'В настоящее время вы редактируете черновик, который был сохранён :timeDiff.',
|
'pages_editing_draft_notification' => 'В настоящее время вы редактируете черновик, который был сохранён :timeDiff.',
|
||||||
'pages_draft_edited_notification' => 'Эта страница была обновлена до этого момента. Рекомендуется отменить этот черновик.',
|
'pages_draft_edited_notification' => 'Эта страница была обновлена до этого момента. Рекомендуется отменить этот черновик.',
|
||||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
'pages_draft_page_changed_since_creation' => 'Эта страница была обновлена с момента создания данного черновика. Рекомендуется выбросить этот черновик или следить за тем, чтобы не перезаписать все изменения на странице.',
|
||||||
'pages_draft_edit_active' => [
|
'pages_draft_edit_active' => [
|
||||||
'start_a' => ':count пользователей начали редактирование этой страницы',
|
'start_a' => ':count пользователей начали редактирование этой страницы',
|
||||||
'start_b' => ':userName начал редактирование этой страницы',
|
'start_b' => ':userName начал редактирование этой страницы',
|
||||||
@@ -258,16 +258,16 @@ return [
|
|||||||
'tags_explain' => "Добавьте теги, чтобы лучше классифицировать ваш контент. \\n Вы можете присвоить значение тегу для более глубокой организации.",
|
'tags_explain' => "Добавьте теги, чтобы лучше классифицировать ваш контент. \\n Вы можете присвоить значение тегу для более глубокой организации.",
|
||||||
'tags_add' => 'Добавить тег',
|
'tags_add' => 'Добавить тег',
|
||||||
'tags_remove' => 'Удалить этот тег',
|
'tags_remove' => 'Удалить этот тег',
|
||||||
'tags_usages' => 'Total tag usages',
|
'tags_usages' => 'Всего использовано тегов',
|
||||||
'tags_assigned_pages' => 'Assigned to Pages',
|
'tags_assigned_pages' => 'Назначено на страницы',
|
||||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
'tags_assigned_chapters' => 'Назначено на главы',
|
||||||
'tags_assigned_books' => 'Assigned to Books',
|
'tags_assigned_books' => 'Назначено на книги',
|
||||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
'tags_assigned_shelves' => 'Назначено на полки',
|
||||||
'tags_x_unique_values' => ':count unique values',
|
'tags_x_unique_values' => 'Уникальные значения: :count',
|
||||||
'tags_all_values' => 'All values',
|
'tags_all_values' => 'Все значения',
|
||||||
'tags_view_tags' => 'View Tags',
|
'tags_view_tags' => 'Посмотреть теги',
|
||||||
'tags_view_existing_tags' => 'View existing tags',
|
'tags_view_existing_tags' => 'Просмотр имеющихся тегов',
|
||||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
'tags_list_empty_hint' => 'Теги можно присваивать через боковую панель редактора страниц или при редактировании сведений о книге, главе или полке.',
|
||||||
'attachments' => 'Вложения',
|
'attachments' => 'Вложения',
|
||||||
'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',
|
'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',
|
||||||
'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',
|
'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ return [
|
|||||||
'saml_no_email_address' => 'Не удалось найти email для этого пользователя в данных, предоставленных внешней системой аутентификации',
|
'saml_no_email_address' => 'Не удалось найти email для этого пользователя в данных, предоставленных внешней системой аутентификации',
|
||||||
'saml_invalid_response_id' => 'Запрос от внешней системы аутентификации не распознается процессом, запущенным этим приложением. Переход назад после входа в систему может вызвать эту проблему.',
|
'saml_invalid_response_id' => 'Запрос от внешней системы аутентификации не распознается процессом, запущенным этим приложением. Переход назад после входа в систему может вызвать эту проблему.',
|
||||||
'saml_fail_authed' => 'Вход с помощью :system не удался, система не предоставила успешную авторизацию',
|
'saml_fail_authed' => 'Вход с помощью :system не удался, система не предоставила успешную авторизацию',
|
||||||
'oidc_already_logged_in' => 'Already logged in',
|
'oidc_already_logged_in' => 'Вход в систему уже произведен',
|
||||||
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
|
'oidc_user_not_registered' => 'Пользователь :name не зарегистрирован и автоматическая регистрация отключена',
|
||||||
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
'oidc_no_email_address' => 'Не удалось найти email этого пользователя в данных, предоставленных внешней системой аутентификации',
|
||||||
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
|
'oidc_fail_authed' => 'Вход в систему с помощью :system не удался, система не обеспечила успешную авторизацию',
|
||||||
'social_no_action_defined' => 'Действие не определено',
|
'social_no_action_defined' => 'Действие не определено',
|
||||||
'social_login_bad_response' => "При попытке входа с :socialAccount произошла ошибка: \\n:error",
|
'social_login_bad_response' => "При попытке входа с :socialAccount произошла ошибка: \\n:error",
|
||||||
'social_account_in_use' => 'Этот :socialAccount аккаунт уже используется, попробуйте войти с параметрами :socialAccount.',
|
'social_account_in_use' => 'Этот :socialAccount аккаунт уже используется, попробуйте войти с параметрами :socialAccount.',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ return [
|
|||||||
'recycle_bin' => 'Корзина',
|
'recycle_bin' => 'Корзина',
|
||||||
'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',
|
'recycle_bin_desc' => 'Здесь вы можете восстановить удаленные элементы или навсегда удалить их из системы. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры.',
|
||||||
'recycle_bin_deleted_item' => 'Удаленный элемент',
|
'recycle_bin_deleted_item' => 'Удаленный элемент',
|
||||||
'recycle_bin_deleted_parent' => 'Parent',
|
'recycle_bin_deleted_parent' => 'Родительский объект',
|
||||||
'recycle_bin_deleted_by' => 'Удалён',
|
'recycle_bin_deleted_by' => 'Удалён',
|
||||||
'recycle_bin_deleted_at' => 'Время удаления',
|
'recycle_bin_deleted_at' => 'Время удаления',
|
||||||
'recycle_bin_permanently_delete' => 'Удалить навсегда',
|
'recycle_bin_permanently_delete' => 'Удалить навсегда',
|
||||||
@@ -105,7 +105,7 @@ return [
|
|||||||
'recycle_bin_restore_list' => 'Элементы для восстановления',
|
'recycle_bin_restore_list' => 'Элементы для восстановления',
|
||||||
'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.',
|
'recycle_bin_restore_confirm' => 'Это действие восстановит удаленный элемент, включая дочерние, в исходное место. Если исходное место было удалено и теперь находится в корзине, родительский элемент также необходимо будет восстановить.',
|
||||||
'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.',
|
'recycle_bin_restore_deleted_parent' => 'Родитель этого элемента также был удален. Элементы будут удалены до тех пор, пока этот родитель не будет восстановлен.',
|
||||||
'recycle_bin_restore_parent' => 'Restore Parent',
|
'recycle_bin_restore_parent' => 'Восстановить родительский объект',
|
||||||
'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.',
|
'recycle_bin_destroy_notification' => 'Удалено :count элементов из корзины.',
|
||||||
'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины',
|
'recycle_bin_restore_notification' => 'Восстановлено :count элементов из корзины',
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ return [
|
|||||||
'role_details' => 'Детали роли',
|
'role_details' => 'Детали роли',
|
||||||
'role_name' => 'Название роли',
|
'role_name' => 'Название роли',
|
||||||
'role_desc' => 'Краткое описание роли',
|
'role_desc' => 'Краткое описание роли',
|
||||||
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
|
'role_mfa_enforced' => 'Требует многофакторной аутентификации',
|
||||||
'role_external_auth_id' => 'Внешние ID авторизации',
|
'role_external_auth_id' => 'Внешние ID авторизации',
|
||||||
'role_system' => 'Системные разрешения',
|
'role_system' => 'Системные разрешения',
|
||||||
'role_manage_users' => 'Управление пользователями',
|
'role_manage_users' => 'Управление пользователями',
|
||||||
@@ -149,7 +149,7 @@ return [
|
|||||||
'role_manage_page_templates' => 'Управление шаблонами страниц',
|
'role_manage_page_templates' => 'Управление шаблонами страниц',
|
||||||
'role_access_api' => 'Доступ к системному API',
|
'role_access_api' => 'Доступ к системному API',
|
||||||
'role_manage_settings' => 'Управление настройками приложения',
|
'role_manage_settings' => 'Управление настройками приложения',
|
||||||
'role_export_content' => 'Export content',
|
'role_export_content' => 'Экспорт контента',
|
||||||
'role_asset' => 'Права доступа к материалам',
|
'role_asset' => 'Права доступа к материалам',
|
||||||
'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.',
|
'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.',
|
||||||
'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',
|
'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',
|
||||||
@@ -209,7 +209,7 @@ return [
|
|||||||
'users_api_tokens_docs' => 'Документация',
|
'users_api_tokens_docs' => 'Документация',
|
||||||
'users_mfa' => 'Двухфакторная аутентификация',
|
'users_mfa' => 'Двухфакторная аутентификация',
|
||||||
'users_mfa_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.',
|
'users_mfa_desc' => 'Двухфакторная аутентификация повышает степень безопасности вашей учетной записи.',
|
||||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
'users_mfa_x_methods' => 'методов настроено :count|методов сконфигурировано :count',
|
||||||
'users_mfa_configure' => 'Настройка методов',
|
'users_mfa_configure' => 'Настройка методов',
|
||||||
|
|
||||||
// API Tokens
|
// API Tokens
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ return [
|
|||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Setup 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_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
||||||
'mfa_setup_configured' => 'Already configured',
|
'mfa_setup_configured' => 'Zaten yapılandırıldı',
|
||||||
'mfa_setup_reconfigure' => 'Reconfigure',
|
'mfa_setup_reconfigure' => 'Yeniden yapılandır',
|
||||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
'mfa_setup_remove_confirmation' => '2 adımlı doğrulamayı kaldırmak istediğinize emin misiniz?',
|
||||||
'mfa_setup_action' => 'Setup',
|
'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_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
||||||
'mfa_option_totp_title' => 'Mobile App',
|
'mfa_option_totp_title' => 'Mobil Uygulama',
|
||||||
'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_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_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_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
|
||||||
@@ -106,5 +106,5 @@ return [
|
|||||||
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
|
'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_backup_code_enter_here' => 'Enter backup code here',
|
||||||
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
|
'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_setup_login_notification' => '2 adımlı doğrulama ayarlandı, Lütfen 2 adımlı doğrulama kullanarak yeniden giriş yapınız.',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => 'Будь ласка, підтвердьте свою адресу електронної пошти, натиснувши кнопку нижче:',
|
'email_confirm_text' => 'Будь ласка, підтвердьте свою адресу електронної пошти, натиснувши кнопку нижче:',
|
||||||
'email_confirm_action' => 'Підтвердити Email',
|
'email_confirm_action' => 'Підтвердити Email',
|
||||||
'email_confirm_send_error' => 'Необхідно підтвердження електронною поштою, але система не змогла надіслати електронний лист. Зверніться до адміністратора, щоб правильно налаштувати електронну пошту.',
|
'email_confirm_send_error' => 'Необхідно підтвердження електронною поштою, але система не змогла надіслати електронний лист. Зверніться до адміністратора, щоб правильно налаштувати електронну пошту.',
|
||||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
'email_confirm_success' => 'Ваша адреса електронної пошти була підтверджена! Тепер ви можете увійти в систему, використовуючи цю адресу електронної пошти.',
|
||||||
'email_confirm_resent' => 'Лист з підтвердженням надіслано, перевірте свою пошту.',
|
'email_confirm_resent' => 'Лист з підтвердженням надіслано, перевірте свою пошту.',
|
||||||
|
|
||||||
'email_not_confirmed' => 'Адресу електронної скриньки не підтверджено',
|
'email_not_confirmed' => 'Адресу електронної скриньки не підтверджено',
|
||||||
@@ -71,40 +71,40 @@ return [
|
|||||||
'user_invite_page_welcome' => 'Ласкаво просимо до :appName!',
|
'user_invite_page_welcome' => 'Ласкаво просимо до :appName!',
|
||||||
'user_invite_page_text' => 'Для завершення процесу створення облікового запису та отримання доступу вам потрібно задати пароль, який буде використовуватися для входу в :appName в майбутньому.',
|
'user_invite_page_text' => 'Для завершення процесу створення облікового запису та отримання доступу вам потрібно задати пароль, який буде використовуватися для входу в :appName в майбутньому.',
|
||||||
'user_invite_page_confirm_button' => 'Підтвердити пароль',
|
'user_invite_page_confirm_button' => 'Підтвердити пароль',
|
||||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
'user_invite_success_login' => 'Пароль встановлено, ви повинні увійти в систему, використовуючи свій встановлений пароль для доступу :appNam!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Setup Multi-Factor Authentication',
|
'mfa_setup' => 'Налаштувати двофакторну автентифікацію',
|
||||||
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
'mfa_setup_desc' => 'Двофакторна аутентифікація додає ще один рівень безпеки для вашого облікового запису.',
|
||||||
'mfa_setup_configured' => 'Already configured',
|
'mfa_setup_configured' => 'Вже налаштовано',
|
||||||
'mfa_setup_reconfigure' => 'Reconfigure',
|
'mfa_setup_reconfigure' => 'Переналаштувати',
|
||||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
'mfa_setup_remove_confirmation' => 'Ви впевнені, що хочете видалити цей метод багатофакторної автентифікації?',
|
||||||
'mfa_setup_action' => 'Setup',
|
'mfa_setup_action' => 'Встановлення',
|
||||||
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
'mfa_backup_codes_usage_limit_warning' => 'Залишилося менше 5 резервних кодів, Будь ласка, створіть та збережіть новий набір до того, як у вас не вистачає кодів, щоб запобігти блокуванню вашої обліковки.',
|
||||||
'mfa_option_totp_title' => 'Mobile App',
|
'mfa_option_totp_title' => 'Мобільний додаток',
|
||||||
'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_totp_desc' => 'Для використання багатофакторної автентифікації вам потрібен мобільний додаток, який підтримує TOTP такі як Google Authenticator, Authy або Microsoft Authenticator.',
|
||||||
'mfa_option_backup_codes_title' => 'Backup Codes',
|
'mfa_option_backup_codes_title' => 'Резервні коди',
|
||||||
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
|
'mfa_option_backup_codes_desc' => 'Безпечно зберігайте набір резервних кодів з використанням одноразового використання, які можна увійти, щоб підтвердити свою особу.',
|
||||||
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
|
'mfa_gen_confirm_and_enable' => 'Підтвердити та увімкнути',
|
||||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
'mfa_gen_backup_codes_title' => 'Налаштування резервних кодів',
|
||||||
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
|
'mfa_gen_backup_codes_desc' => 'Зберігайте список кодів в безпечному місці. Для доступу до системи ви зможете використовувати один з кодів як другий механізм аутентифікації.',
|
||||||
'mfa_gen_backup_codes_download' => 'Download Codes',
|
'mfa_gen_backup_codes_download' => 'Завантажити коди',
|
||||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
'mfa_gen_backup_codes_usage_warning' => 'Кожний код можна використати лише один раз',
|
||||||
'mfa_gen_totp_title' => 'Mobile App Setup',
|
'mfa_gen_totp_title' => 'Налаштування мобільного додатка',
|
||||||
'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_desc' => 'Для використання багатофакторної автентифікації вам потрібен мобільний додаток, який підтримує TOTP такі як Google Authenticator, Authy або Microsoft Authenticator.',
|
||||||
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
|
'mfa_gen_totp_scan' => 'Проскануйте QR-код внизу за допомогою бажаного додатку для аутентифікації, щоб розпочати.',
|
||||||
'mfa_gen_totp_verify_setup' => 'Verify Setup',
|
'mfa_gen_totp_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_verify_setup_desc' => 'Переконайтеся, що все працює, ввівши код, згенерований у вашому додатку аутентифікації, в полі вводу нижче:',
|
||||||
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
'mfa_gen_totp_provide_code_here' => 'Вкажіть код вашої програми тут',
|
||||||
'mfa_verify_access' => 'Verify Access',
|
'mfa_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_access_desc' => 'Ваш обліковий запис користувача вимагає підтвердження за допомогою додаткового рівня перевірки, перш ніж отримати доступ. Перевірте, використовуючи один з ваших налаштованих способів для продовження.',
|
||||||
'mfa_verify_no_methods' => 'No Methods Configured',
|
'mfa_verify_no_methods' => 'Немає налаштованих методів',
|
||||||
'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_no_methods_desc' => 'Для вашого облікового запису не знайдено жодних методів багатофакторної аутентифікації. Вам потрібно буде налаштувати хоча б один спосіб перш ніж отримати доступ.',
|
||||||
'mfa_verify_use_totp' => 'Verify using a mobile app',
|
'mfa_verify_use_totp' => 'Перевірити за допомогою мобільного додатку',
|
||||||
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
|
'mfa_verify_use_backup_codes' => 'Перевірка використовуючи резервний код',
|
||||||
'mfa_verify_backup_code' => 'Backup Code',
|
'mfa_verify_backup_code' => 'Резервний код',
|
||||||
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
|
'mfa_verify_backup_code_desc' => 'Введіть один з резервних кодів нижче:',
|
||||||
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
|
'mfa_verify_backup_code_enter_here' => 'Введіть резервний код тут',
|
||||||
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
|
'mfa_verify_totp_desc' => 'Введіть код, згенерований за допомогою мобільного додатку:',
|
||||||
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
|
'mfa_setup_login_notification' => 'Налаштовано багатофакторний метод аутентифікації. Будь ласка, зараз увійдіть в систему знову, використовуючи налаштований метод.',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ return [
|
|||||||
'reset' => 'Скинути',
|
'reset' => 'Скинути',
|
||||||
'remove' => 'Видалити',
|
'remove' => 'Видалити',
|
||||||
'add' => 'Додати',
|
'add' => 'Додати',
|
||||||
'configure' => 'Configure',
|
'configure' => 'Налаштувати',
|
||||||
'fullscreen' => 'На весь екран',
|
'fullscreen' => 'На весь екран',
|
||||||
'favourite' => 'Favourite',
|
'favourite' => 'Улюблене',
|
||||||
'unfavourite' => 'Unfavourite',
|
'unfavourite' => 'Прибрати з обраного',
|
||||||
'next' => 'Next',
|
'next' => 'Уперед',
|
||||||
'previous' => 'Previous',
|
'previous' => 'Назад',
|
||||||
'filter_active' => 'Active Filter:',
|
'filter_active' => 'Активний фільтр:',
|
||||||
'filter_clear' => 'Clear Filter',
|
'filter_clear' => 'Очистити фільтр',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => 'Параметри сортування',
|
'sort_options' => 'Параметри сортування',
|
||||||
@@ -63,7 +63,7 @@ return [
|
|||||||
'no_activity' => 'Немає активності для показу',
|
'no_activity' => 'Немає активності для показу',
|
||||||
'no_items' => 'Немає доступних елементів',
|
'no_items' => 'Немає доступних елементів',
|
||||||
'back_to_top' => 'Повернутися до початку',
|
'back_to_top' => 'Повернутися до початку',
|
||||||
'skip_to_main_content' => 'Skip to main content',
|
'skip_to_main_content' => 'Перейти до змісту',
|
||||||
'toggle_details' => 'Подробиці',
|
'toggle_details' => 'Подробиці',
|
||||||
'toggle_thumbnails' => 'Мініатюри',
|
'toggle_thumbnails' => 'Мініатюри',
|
||||||
'details' => 'Деталі',
|
'details' => 'Деталі',
|
||||||
@@ -73,7 +73,7 @@ return [
|
|||||||
'breadcrumb' => 'Навігація',
|
'breadcrumb' => 'Навігація',
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
'header_menu_expand' => 'Expand Header Menu',
|
'header_menu_expand' => 'Розгорнути меню заголовка',
|
||||||
'profile_menu' => 'Меню профілю',
|
'profile_menu' => 'Меню профілю',
|
||||||
'view_profile' => 'Переглянути профіль',
|
'view_profile' => 'Переглянути профіль',
|
||||||
'edit_profile' => 'Редагувати профіль',
|
'edit_profile' => 'Редагувати профіль',
|
||||||
@@ -82,9 +82,9 @@ return [
|
|||||||
|
|
||||||
// Layout tabs
|
// Layout tabs
|
||||||
'tab_info' => 'Інфо',
|
'tab_info' => 'Інфо',
|
||||||
'tab_info_label' => 'Tab: Show Secondary Information',
|
'tab_info_label' => 'Вкладка: показувати додаткову інформацію',
|
||||||
'tab_content' => 'Вміст',
|
'tab_content' => 'Вміст',
|
||||||
'tab_content_label' => 'Tab: Show Primary Content',
|
'tab_content_label' => 'Вкладка: Показати основний вміст',
|
||||||
|
|
||||||
// Email Content
|
// Email Content
|
||||||
'email_action_help' => 'Якщо у вас виникають проблеми при натисканні кнопки ":actionText", скопіюйте та вставте URL у свій веб-браузер:',
|
'email_action_help' => 'Якщо у вас виникають проблеми при натисканні кнопки ":actionText", скопіюйте та вставте URL у свій веб-браузер:',
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ return [
|
|||||||
'images' => 'Зображення',
|
'images' => 'Зображення',
|
||||||
'my_recent_drafts' => 'Мої останні чернетки',
|
'my_recent_drafts' => 'Мої останні чернетки',
|
||||||
'my_recently_viewed' => 'Мої недавні перегляди',
|
'my_recently_viewed' => 'Мої недавні перегляди',
|
||||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
'my_most_viewed_favourites' => 'Мої найпопулярніші улюблені',
|
||||||
'my_favourites' => 'My Favourites',
|
'my_favourites' => 'Моє обране',
|
||||||
'no_pages_viewed' => 'Ви не переглядали жодної сторінки',
|
'no_pages_viewed' => 'Ви не переглядали жодної сторінки',
|
||||||
'no_pages_recently_created' => 'Не було створено жодної сторінки',
|
'no_pages_recently_created' => 'Не було створено жодної сторінки',
|
||||||
'no_pages_recently_updated' => 'Немає недавно оновлених сторінок',
|
'no_pages_recently_updated' => 'Немає недавно оновлених сторінок',
|
||||||
@@ -36,7 +36,7 @@ return [
|
|||||||
'export_html' => 'Вбудований веб-файл',
|
'export_html' => 'Вбудований веб-файл',
|
||||||
'export_pdf' => 'PDF файл',
|
'export_pdf' => 'PDF файл',
|
||||||
'export_text' => 'Текстовий файл',
|
'export_text' => 'Текстовий файл',
|
||||||
'export_md' => 'Markdown File',
|
'export_md' => 'Файл розмітки',
|
||||||
|
|
||||||
// Permissions and restrictions
|
// Permissions and restrictions
|
||||||
'permissions' => 'Дозволи',
|
'permissions' => 'Дозволи',
|
||||||
@@ -99,7 +99,7 @@ return [
|
|||||||
'shelves_permissions' => 'Дозволи на книжкову полицю',
|
'shelves_permissions' => 'Дозволи на книжкову полицю',
|
||||||
'shelves_permissions_updated' => 'Дозволи на книжкову полицю оновлено',
|
'shelves_permissions_updated' => 'Дозволи на книжкову полицю оновлено',
|
||||||
'shelves_permissions_active' => 'Діючі дозволи на книжкову полицю',
|
'shelves_permissions_active' => 'Діючі дозволи на книжкову полицю',
|
||||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
'shelves_permissions_cascade_warning' => 'Дозволи на поличках не потрапляють автоматично каскадом до книг. Це тому, що книга може існувати на кілька полиць. Дозволи, однак можна скопіювати до дочірніх книг, використовуючи нижченаведену опцію.',
|
||||||
'shelves_copy_permissions_to_books' => 'Копіювати дозволи на книги',
|
'shelves_copy_permissions_to_books' => 'Копіювати дозволи на книги',
|
||||||
'shelves_copy_permissions' => 'Копіювати дозволи',
|
'shelves_copy_permissions' => 'Копіювати дозволи',
|
||||||
'shelves_copy_permissions_explain' => 'Це застосовує поточні налаштування дозволів цієї книжкової полиці до всіх книг, що містяться всередині. Перш ніж активувати, переконайтесь що будь-які зміни дозволів цієї книжкової полиці були збережені.',
|
'shelves_copy_permissions_explain' => 'Це застосовує поточні налаштування дозволів цієї книжкової полиці до всіх книг, що містяться всередині. Перш ніж активувати, переконайтесь що будь-які зміни дозволів цієї книжкової полиці були збережені.',
|
||||||
@@ -234,7 +234,7 @@ return [
|
|||||||
'pages_initial_name' => 'Нова сторінка',
|
'pages_initial_name' => 'Нова сторінка',
|
||||||
'pages_editing_draft_notification' => 'Ви наразі редагуєте чернетку, що була збережена останньою :timeDiff.',
|
'pages_editing_draft_notification' => 'Ви наразі редагуєте чернетку, що була збережена останньою :timeDiff.',
|
||||||
'pages_draft_edited_notification' => 'З того часу ця сторінка була оновлена. Рекомендуємо відмовитися від цього проекту.',
|
'pages_draft_edited_notification' => 'З того часу ця сторінка була оновлена. Рекомендуємо відмовитися від цього проекту.',
|
||||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
'pages_draft_page_changed_since_creation' => 'Ця сторінка була оновлена, оскільки була створена ця чернетка. Рекомендується відхилити цей проект або перейматися тим, що ви не перезапишете будь-які зміни в сторінках.',
|
||||||
'pages_draft_edit_active' => [
|
'pages_draft_edit_active' => [
|
||||||
'start_a' => ':count користувачі(в) почали редагувати цю сторінку',
|
'start_a' => ':count користувачі(в) почали редагувати цю сторінку',
|
||||||
'start_b' => ':userName розпочав редагування цієї сторінки',
|
'start_b' => ':userName розпочав редагування цієї сторінки',
|
||||||
@@ -258,16 +258,16 @@ return [
|
|||||||
'tags_explain' => "Додайте кілька тегів, щоб краще класифікувати ваш вміст. \n Ви можете присвоїти значення тегу для більш глибокої організації.",
|
'tags_explain' => "Додайте кілька тегів, щоб краще класифікувати ваш вміст. \n Ви можете присвоїти значення тегу для більш глибокої організації.",
|
||||||
'tags_add' => 'Додати ще один тег',
|
'tags_add' => 'Додати ще один тег',
|
||||||
'tags_remove' => 'Видалити цей тег',
|
'tags_remove' => 'Видалити цей тег',
|
||||||
'tags_usages' => 'Total tag usages',
|
'tags_usages' => 'Усього тегів використано',
|
||||||
'tags_assigned_pages' => 'Assigned to Pages',
|
'tags_assigned_pages' => 'Призначено до сторінок',
|
||||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
'tags_assigned_chapters' => 'Призначені до груп',
|
||||||
'tags_assigned_books' => 'Assigned to Books',
|
'tags_assigned_books' => 'Призначено до книг',
|
||||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
'tags_assigned_shelves' => 'Призначені до полиць',
|
||||||
'tags_x_unique_values' => ':count unique values',
|
'tags_x_unique_values' => ':count унікальних значень',
|
||||||
'tags_all_values' => 'All values',
|
'tags_all_values' => 'Всі значення',
|
||||||
'tags_view_tags' => 'View Tags',
|
'tags_view_tags' => 'Перегляд міток',
|
||||||
'tags_view_existing_tags' => 'View existing tags',
|
'tags_view_existing_tags' => 'Перегляд існуючих тегів',
|
||||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
'tags_list_empty_hint' => 'Теги можуть бути призначені через бічну панель редактора сторінки, або під час редагування деталей книги, глави чи полиці.',
|
||||||
'attachments' => 'Вкладення',
|
'attachments' => 'Вкладення',
|
||||||
'attachments_explain' => 'Завантажте файли, або додайте посилання, які відображатимуться на вашій сторінці. Їх буде видно на бічній панелі сторінки.',
|
'attachments_explain' => 'Завантажте файли, або додайте посилання, які відображатимуться на вашій сторінці. Їх буде видно на бічній панелі сторінки.',
|
||||||
'attachments_explain_instant_save' => 'Зміни тут зберігаються миттєво.',
|
'attachments_explain_instant_save' => 'Зміни тут зберігаються миттєво.',
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ return [
|
|||||||
'saml_no_email_address' => 'Не вдалося знайти електронну адресу для цього користувача у даних, наданих зовнішньою системою аутентифікації',
|
'saml_no_email_address' => 'Не вдалося знайти електронну адресу для цього користувача у даних, наданих зовнішньою системою аутентифікації',
|
||||||
'saml_invalid_response_id' => 'Запит із зовнішньої системи аутентифікації не розпізнається процесом, розпочатим цим додатком. Повернення назад після входу могла спричинити цю проблему.',
|
'saml_invalid_response_id' => 'Запит із зовнішньої системи аутентифікації не розпізнається процесом, розпочатим цим додатком. Повернення назад після входу могла спричинити цю проблему.',
|
||||||
'saml_fail_authed' => 'Вхід із використанням «:system» не вдався, система не здійснила успішну авторизацію',
|
'saml_fail_authed' => 'Вхід із використанням «:system» не вдався, система не здійснила успішну авторизацію',
|
||||||
'oidc_already_logged_in' => 'Already logged in',
|
'oidc_already_logged_in' => 'Вже ввійшли в систему',
|
||||||
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
|
'oidc_user_not_registered' => 'Користувач :name не зареєстровано і автоматична реєстрація відключена',
|
||||||
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
'oidc_no_email_address' => 'Не вдалося знайти адресу електронної пошти для цього користувача у даних, наданих зовнішньою системою автентифікації',
|
||||||
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
|
'oidc_fail_authed' => 'Увійти за допомогою :system не вдалося, система не надала успішної авторизації',
|
||||||
'social_no_action_defined' => 'Жодних дій не визначено',
|
'social_no_action_defined' => 'Жодних дій не визначено',
|
||||||
'social_login_bad_response' => "Помилка, отримана під час входу з :socialAccount помилка : \n:error",
|
'social_login_bad_response' => "Помилка, отримана під час входу з :socialAccount помилка : \n:error",
|
||||||
'social_account_in_use' => 'Цей :socialAccount обліковий запис вже використовується, спробуйте ввійти з параметрами :socialAccount.',
|
'social_account_in_use' => 'Цей :socialAccount обліковий запис вже використовується, спробуйте ввійти з параметрами :socialAccount.',
|
||||||
@@ -87,9 +87,9 @@ return [
|
|||||||
'404_page_not_found' => 'Сторінку не знайдено',
|
'404_page_not_found' => 'Сторінку не знайдено',
|
||||||
'sorry_page_not_found' => 'Вибачте, сторінку, яку ви шукали, не знайдено.',
|
'sorry_page_not_found' => 'Вибачте, сторінку, яку ви шукали, не знайдено.',
|
||||||
'sorry_page_not_found_permission_warning' => 'Якщо ви очікували що ця сторінки існує – можливо у вас немає дозволу на її перегляд.',
|
'sorry_page_not_found_permission_warning' => 'Якщо ви очікували що ця сторінки існує – можливо у вас немає дозволу на її перегляд.',
|
||||||
'image_not_found' => 'Image Not Found',
|
'image_not_found' => 'Зображення не знайдено',
|
||||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
'image_not_found_subtitle' => 'Вибачте, файл зображення, що ви шукали, не знайдено.',
|
||||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
'image_not_found_details' => 'Якщо ви очікували існування цього зображення, його, можливо, було видалено.',
|
||||||
'return_home' => 'Повернутися на головну',
|
'return_home' => 'Повернутися на головну',
|
||||||
'error_occurred' => 'Виникла помилка',
|
'error_occurred' => 'Виникла помилка',
|
||||||
'app_down' => ':appName зараз недоступний',
|
'app_down' => ':appName зараз недоступний',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ return [
|
|||||||
'app_name' => 'Назва програми',
|
'app_name' => 'Назва програми',
|
||||||
'app_name_desc' => 'Ця назва показується у заголовку та в усіх листах.',
|
'app_name_desc' => 'Ця назва показується у заголовку та в усіх листах.',
|
||||||
'app_name_header' => 'Показати назву програми в заголовку',
|
'app_name_header' => 'Показати назву програми в заголовку',
|
||||||
'app_public_access' => 'Публічнй доступ',
|
'app_public_access' => 'Публічний доступ',
|
||||||
'app_public_access_desc' => 'Увімкнення цієї опції дозволить відвідувачам, які не увійшли в систему, отримати доступ до вмісту у вашому екземплярі BookStack.',
|
'app_public_access_desc' => 'Увімкнення цієї опції дозволить відвідувачам, які не увійшли в систему, отримати доступ до вмісту у вашому екземплярі BookStack.',
|
||||||
'app_public_access_desc_guest' => 'Доступ для публічних відвідувачів можна контролювати через користувача "Гість".',
|
'app_public_access_desc_guest' => 'Доступ для публічних відвідувачів можна контролювати через користувача "Гість".',
|
||||||
'app_public_access_toggle' => 'Дозволити публічний доступ',
|
'app_public_access_toggle' => 'Дозволити публічний доступ',
|
||||||
@@ -92,7 +92,7 @@ return [
|
|||||||
'recycle_bin' => 'Кошик',
|
'recycle_bin' => 'Кошик',
|
||||||
'recycle_bin_desc' => 'Тут ви можете відновити видалені елементи, або назавжди видалити їх із системи. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.',
|
'recycle_bin_desc' => 'Тут ви можете відновити видалені елементи, або назавжди видалити їх із системи. Цей список нефільтрований, на відміну від подібних списків активності в системі, де застосовуються фільтри дозволів.',
|
||||||
'recycle_bin_deleted_item' => 'Виадлений елемент',
|
'recycle_bin_deleted_item' => 'Виадлений елемент',
|
||||||
'recycle_bin_deleted_parent' => 'Parent',
|
'recycle_bin_deleted_parent' => 'Батьківський',
|
||||||
'recycle_bin_deleted_by' => 'Ким видалено',
|
'recycle_bin_deleted_by' => 'Ким видалено',
|
||||||
'recycle_bin_deleted_at' => 'Час видалення',
|
'recycle_bin_deleted_at' => 'Час видалення',
|
||||||
'recycle_bin_permanently_delete' => 'Видалити остаточно',
|
'recycle_bin_permanently_delete' => 'Видалити остаточно',
|
||||||
@@ -105,7 +105,7 @@ return [
|
|||||||
'recycle_bin_restore_list' => 'Елементи для відновлення',
|
'recycle_bin_restore_list' => 'Елементи для відновлення',
|
||||||
'recycle_bin_restore_confirm' => 'Ця дія відновить видалений елемент у початкове місце, включаючи всі дочірні елементи. Якщо вихідне розташування відтоді було видалено, і знаходиться у кошику, батьківський елемент також потрібно буде відновити.',
|
'recycle_bin_restore_confirm' => 'Ця дія відновить видалений елемент у початкове місце, включаючи всі дочірні елементи. Якщо вихідне розташування відтоді було видалено, і знаходиться у кошику, батьківський елемент також потрібно буде відновити.',
|
||||||
'recycle_bin_restore_deleted_parent' => 'Батьківський елемент цього об\'єкта також був видалений. Вони залишатимуться видаленими, доки батьківський елемент також не буде відновлений.',
|
'recycle_bin_restore_deleted_parent' => 'Батьківський елемент цього об\'єкта також був видалений. Вони залишатимуться видаленими, доки батьківський елемент також не буде відновлений.',
|
||||||
'recycle_bin_restore_parent' => 'Restore Parent',
|
'recycle_bin_restore_parent' => 'Відновити батьківську',
|
||||||
'recycle_bin_destroy_notification' => 'Видалено :count елементів із кошика.',
|
'recycle_bin_destroy_notification' => 'Видалено :count елементів із кошика.',
|
||||||
'recycle_bin_restore_notification' => 'Відновлено :count елементів із кошика.',
|
'recycle_bin_restore_notification' => 'Відновлено :count елементів із кошика.',
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ return [
|
|||||||
'audit_table_user' => 'Користувач',
|
'audit_table_user' => 'Користувач',
|
||||||
'audit_table_event' => 'Подія',
|
'audit_table_event' => 'Подія',
|
||||||
'audit_table_related' => 'Пов’язаний елемент',
|
'audit_table_related' => 'Пов’язаний елемент',
|
||||||
'audit_table_ip' => 'IP Address',
|
'audit_table_ip' => 'IP-адреса',
|
||||||
'audit_table_date' => 'Дата активності',
|
'audit_table_date' => 'Дата активності',
|
||||||
'audit_date_from' => 'Діапазон дат від',
|
'audit_date_from' => 'Діапазон дат від',
|
||||||
'audit_date_to' => 'Діапазон дат до',
|
'audit_date_to' => 'Діапазон дат до',
|
||||||
@@ -139,7 +139,7 @@ return [
|
|||||||
'role_details' => 'Деталі ролі',
|
'role_details' => 'Деталі ролі',
|
||||||
'role_name' => 'Назва ролі',
|
'role_name' => 'Назва ролі',
|
||||||
'role_desc' => 'Короткий опис ролі',
|
'role_desc' => 'Короткий опис ролі',
|
||||||
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
|
'role_mfa_enforced' => 'Потрібна двофактова автентифікація',
|
||||||
'role_external_auth_id' => 'Зовнішні ID автентифікації',
|
'role_external_auth_id' => 'Зовнішні ID автентифікації',
|
||||||
'role_system' => 'Системні дозволи',
|
'role_system' => 'Системні дозволи',
|
||||||
'role_manage_users' => 'Керування користувачами',
|
'role_manage_users' => 'Керування користувачами',
|
||||||
@@ -149,7 +149,7 @@ return [
|
|||||||
'role_manage_page_templates' => 'Управління шаблонами сторінок',
|
'role_manage_page_templates' => 'Управління шаблонами сторінок',
|
||||||
'role_access_api' => 'Доступ до системного API',
|
'role_access_api' => 'Доступ до системного API',
|
||||||
'role_manage_settings' => 'Керування налаштуваннями програми',
|
'role_manage_settings' => 'Керування налаштуваннями програми',
|
||||||
'role_export_content' => 'Export content',
|
'role_export_content' => 'Вміст експорту',
|
||||||
'role_asset' => 'Дозволи',
|
'role_asset' => 'Дозволи',
|
||||||
'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.',
|
'roles_system_warning' => 'Майте на увазі, що доступ до будь-якого з вищезазначених трьох дозволів може дозволити користувачеві змінювати власні привілеї або привілеї інших в системі. Ролі з цими дозволами призначайте лише довіреним користувачам.',
|
||||||
'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.',
|
'role_asset_desc' => 'Ці дозволи контролюють стандартні доступи всередині системи. Права на книги, розділи та сторінки перевизначать ці дозволи.',
|
||||||
@@ -207,10 +207,10 @@ return [
|
|||||||
'users_api_tokens_create' => 'Створити токен',
|
'users_api_tokens_create' => 'Створити токен',
|
||||||
'users_api_tokens_expires' => 'Закінчується',
|
'users_api_tokens_expires' => 'Закінчується',
|
||||||
'users_api_tokens_docs' => 'Документація API',
|
'users_api_tokens_docs' => 'Документація API',
|
||||||
'users_mfa' => 'Multi-Factor Authentication',
|
'users_mfa' => 'Багатофакторна Автентифікація',
|
||||||
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
'users_mfa_desc' => 'Двофакторна аутентифікація додає ще один рівень безпеки для вашого облікового запису.',
|
||||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
'users_mfa_x_methods' => ':count метод налаштовано|:count методів налаштовано',
|
||||||
'users_mfa_configure' => 'Configure Methods',
|
'users_mfa_configure' => 'Налаштувати Методи',
|
||||||
|
|
||||||
// API Tokens
|
// API Tokens
|
||||||
'user_api_token_create' => 'Створити токен API',
|
'user_api_token_create' => 'Створити токен API',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ return [
|
|||||||
'alpha_dash' => 'Поле :attribute має містити лише літери, цифри, дефіси та підкреслення.',
|
'alpha_dash' => 'Поле :attribute має містити лише літери, цифри, дефіси та підкреслення.',
|
||||||
'alpha_num' => 'Поле :attribute має містити лише літери та цифри.',
|
'alpha_num' => 'Поле :attribute має містити лише літери та цифри.',
|
||||||
'array' => 'Поле :attribute має бути масивом.',
|
'array' => 'Поле :attribute має бути масивом.',
|
||||||
'backup_codes' => 'The provided code is not valid or has already been used.',
|
'backup_codes' => 'Наданий код є недійсним або вже використаний.',
|
||||||
'before' => 'Поле :attribute має містити дату не пізніше :date.',
|
'before' => 'Поле :attribute має містити дату не пізніше :date.',
|
||||||
'between' => [
|
'between' => [
|
||||||
'numeric' => 'Поле :attribute має бути між :min та :max.',
|
'numeric' => 'Поле :attribute має бути між :min та :max.',
|
||||||
@@ -99,7 +99,7 @@ return [
|
|||||||
],
|
],
|
||||||
'string' => 'Поле :attribute повинне містити текст.',
|
'string' => 'Поле :attribute повинне містити текст.',
|
||||||
'timezone' => 'Поле :attribute повинне містити коректну часову зону.',
|
'timezone' => 'Поле :attribute повинне містити коректну часову зону.',
|
||||||
'totp' => 'The provided code is not valid or has expired.',
|
'totp' => 'Наданий код не є дійсним або прострочений.',
|
||||||
'unique' => 'Вказане значення поля :attribute вже існує.',
|
'unique' => 'Вказане значення поля :attribute вже існує.',
|
||||||
'url' => 'Формат поля :attribute неправильний.',
|
'url' => 'Формат поля :attribute неправильний.',
|
||||||
'uploaded' => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.',
|
'uploaded' => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.',
|
||||||
|
|||||||
@@ -96,12 +96,12 @@ return [
|
|||||||
'mfa_gen_totp_verify_setup' => 'Xác nhận cài đặt',
|
'mfa_gen_totp_verify_setup' => 'Xác nhận cài đặt',
|
||||||
'mfa_gen_totp_verify_setup_desc' => 'Xác nhận rằng tất cả hoạt động bằng cách nhập vào một mã, được tạo ra bởi ứng dụng xác thực của bạn vào ô dưới đây:',
|
'mfa_gen_totp_verify_setup_desc' => 'Xác nhận rằng tất cả hoạt động bằng cách nhập vào một mã, được tạo ra bởi ứng dụng xác thực của bạn vào ô dưới đây:',
|
||||||
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
||||||
'mfa_verify_access' => 'Verify Access',
|
'mfa_verify_access' => 'Xác thực truy cập',
|
||||||
'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_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' => 'Không có phương pháp nào được cấu hình',
|
||||||
'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_no_methods_desc' => 'Tài khoản của bạn chưa đăng ký xác thực nhiều lớp. Bạn cần thiết lập ít nhất một phương pháp trước khi yêu cầu truy cập.',
|
||||||
'mfa_verify_use_totp' => 'Verify using a mobile app',
|
'mfa_verify_use_totp' => 'Xác thực sử dụng mã di động',
|
||||||
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
|
'mfa_verify_use_backup_codes' => 'Xác thực sử dụng mã backup',
|
||||||
'mfa_verify_backup_code' => 'Mã dự phòng',
|
'mfa_verify_backup_code' => 'Mã dự phòng',
|
||||||
'mfa_verify_backup_code_desc' => 'Nhập một trong các mã dự phòng còn lại của bạn vào ô phía dưới:',
|
'mfa_verify_backup_code_desc' => 'Nhập một trong các mã dự phòng còn lại của bạn vào ô phía dưới:',
|
||||||
'mfa_verify_backup_code_enter_here' => 'Nhập mã xác thực của bạn tại đây',
|
'mfa_verify_backup_code_enter_here' => 'Nhập mã xác thực của bạn tại đây',
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ return [
|
|||||||
'bookshelf_delete_notification' => '书架已成功删除',
|
'bookshelf_delete_notification' => '书架已成功删除',
|
||||||
|
|
||||||
// Favourites
|
// Favourites
|
||||||
'favourite_add_notification' => '":name" 已添加到你的收藏',
|
'favourite_add_notification' => '":name" 已添加到您的收藏',
|
||||||
'favourite_remove_notification' => '":name" 已从你的收藏中删除',
|
'favourite_remove_notification' => '":name" 已从您的收藏中删除',
|
||||||
|
|
||||||
// MFA
|
// MFA
|
||||||
'mfa_setup_method_notification' => '多重身份认证设置成功',
|
'mfa_setup_method_notification' => '多重身份认证设置成功',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ return [
|
|||||||
'email_confirm_text' => '请点击下面的按钮确认您的Email地址:',
|
'email_confirm_text' => '请点击下面的按钮确认您的Email地址:',
|
||||||
'email_confirm_action' => '确认Email',
|
'email_confirm_action' => '确认Email',
|
||||||
'email_confirm_send_error' => '需要Email验证,但系统无法发送电子邮件,请联系网站管理员。',
|
'email_confirm_send_error' => '需要Email验证,但系统无法发送电子邮件,请联系网站管理员。',
|
||||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
'email_confirm_success' => '您已成功验证电子邮件地址!您现在可以使用此电子邮件地址登录。',
|
||||||
'email_confirm_resent' => '验证邮件已重新发送,请检查收件箱。',
|
'email_confirm_resent' => '验证邮件已重新发送,请检查收件箱。',
|
||||||
|
|
||||||
'email_not_confirmed' => 'Email地址未验证',
|
'email_not_confirmed' => 'Email地址未验证',
|
||||||
@@ -71,7 +71,7 @@ return [
|
|||||||
'user_invite_page_welcome' => '欢迎来到 :appName!',
|
'user_invite_page_welcome' => '欢迎来到 :appName!',
|
||||||
'user_invite_page_text' => '要完成您的帐户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。',
|
'user_invite_page_text' => '要完成您的帐户并获得访问权限,您需要设置一个密码,该密码将在以后访问时用于登录 :appName。',
|
||||||
'user_invite_page_confirm_button' => '确认密码',
|
'user_invite_page_confirm_button' => '确认密码',
|
||||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
'user_invite_success_login' => '密码已设置,您现在可以使用您设置的密码登录 :appName!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => '设置多重身份认证',
|
'mfa_setup' => '设置多重身份认证',
|
||||||
@@ -92,7 +92,7 @@ return [
|
|||||||
'mfa_gen_backup_codes_usage_warning' => '每个认证码只能使用一次',
|
'mfa_gen_backup_codes_usage_warning' => '每个认证码只能使用一次',
|
||||||
'mfa_gen_totp_title' => '移动设备 App',
|
'mfa_gen_totp_title' => '移动设备 App',
|
||||||
'mfa_gen_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。',
|
'mfa_gen_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP(基于时间的一次性密码算法) 的移动设备 App,如谷歌身份验证器(Google Authenticator)、Authy 或微软身份验证器(Microsoft Authenticator)。',
|
||||||
'mfa_gen_totp_scan' => '要开始操作,请使用你的身份验证 App 扫描下面的二维码。',
|
'mfa_gen_totp_scan' => '要开始操作,请使用您的身份验证 App 扫描下面的二维码。',
|
||||||
'mfa_gen_totp_verify_setup' => '验证设置',
|
'mfa_gen_totp_verify_setup' => '验证设置',
|
||||||
'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的认证码来验证一切是否正常:',
|
'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的认证码来验证一切是否正常:',
|
||||||
'mfa_gen_totp_provide_code_here' => '在此输入您的 App 生成的认证码',
|
'mfa_gen_totp_provide_code_here' => '在此输入您的 App 生成的认证码',
|
||||||
@@ -103,7 +103,7 @@ return [
|
|||||||
'mfa_verify_use_totp' => '使用移动设备 App 进行认证',
|
'mfa_verify_use_totp' => '使用移动设备 App 进行认证',
|
||||||
'mfa_verify_use_backup_codes' => '使用备用认证码进行认证',
|
'mfa_verify_use_backup_codes' => '使用备用认证码进行认证',
|
||||||
'mfa_verify_backup_code' => '备用认证码',
|
'mfa_verify_backup_code' => '备用认证码',
|
||||||
'mfa_verify_backup_code_desc' => '在下面输入你的其中一个备用认证码:',
|
'mfa_verify_backup_code_desc' => '在下面输入您的其中一个备用认证码:',
|
||||||
'mfa_verify_backup_code_enter_here' => '在这里输入备用认证码',
|
'mfa_verify_backup_code_enter_here' => '在这里输入备用认证码',
|
||||||
'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的认证码:',
|
'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的认证码:',
|
||||||
'mfa_setup_login_notification' => '多重身份认证已设置,请使用新配置的方法重新登录。',
|
'mfa_setup_login_notification' => '多重身份认证已设置,请使用新配置的方法重新登录。',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ return [
|
|||||||
'description' => '概要',
|
'description' => '概要',
|
||||||
'role' => '角色',
|
'role' => '角色',
|
||||||
'cover_image' => '封面图片',
|
'cover_image' => '封面图片',
|
||||||
'cover_image_description' => '该图像大小需要为440x250px。',
|
'cover_image_description' => '此图像大小应约为 440x250 像素。',
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
'actions' => '操作',
|
'actions' => '操作',
|
||||||
@@ -45,8 +45,8 @@ return [
|
|||||||
'unfavourite' => '取消收藏',
|
'unfavourite' => '取消收藏',
|
||||||
'next' => '下一页',
|
'next' => '下一页',
|
||||||
'previous' => '上一页',
|
'previous' => '上一页',
|
||||||
'filter_active' => 'Active Filter:',
|
'filter_active' => '标签过滤器:',
|
||||||
'filter_clear' => 'Clear Filter',
|
'filter_clear' => '清除过滤器',
|
||||||
|
|
||||||
// Sort Options
|
// Sort Options
|
||||||
'sort_options' => '排序选项',
|
'sort_options' => '排序选项',
|
||||||
@@ -75,8 +75,8 @@ return [
|
|||||||
// Header
|
// Header
|
||||||
'header_menu_expand' => '展开标头菜单',
|
'header_menu_expand' => '展开标头菜单',
|
||||||
'profile_menu' => '个人资料',
|
'profile_menu' => '个人资料',
|
||||||
'view_profile' => '查看资料',
|
'view_profile' => '查看个人资料',
|
||||||
'edit_profile' => '编辑资料',
|
'edit_profile' => '编辑个人资料',
|
||||||
'dark_mode' => '夜间模式',
|
'dark_mode' => '夜间模式',
|
||||||
'light_mode' => '日间模式',
|
'light_mode' => '日间模式',
|
||||||
|
|
||||||
|
|||||||
@@ -254,20 +254,20 @@ return [
|
|||||||
'tag' => '标签',
|
'tag' => '标签',
|
||||||
'tags' => '标签',
|
'tags' => '标签',
|
||||||
'tag_name' => '标签名称',
|
'tag_name' => '标签名称',
|
||||||
'tag_value' => '标签值 (Optional)',
|
'tag_value' => '标签值 (可选)',
|
||||||
'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更深入的组织。",
|
'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更好的进行管理。",
|
||||||
'tags_add' => '添加另一个标签',
|
'tags_add' => '添加另一个标签',
|
||||||
'tags_remove' => '删除此标签',
|
'tags_remove' => '删除此标签',
|
||||||
'tags_usages' => 'Total tag usages',
|
'tags_usages' => '标签总使用量',
|
||||||
'tags_assigned_pages' => 'Assigned to Pages',
|
'tags_assigned_pages' => '有这个标签的页面',
|
||||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
'tags_assigned_chapters' => '有这个标签的章节',
|
||||||
'tags_assigned_books' => 'Assigned to Books',
|
'tags_assigned_books' => '有这个标签的图书',
|
||||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
'tags_assigned_shelves' => '有这个标签的书架',
|
||||||
'tags_x_unique_values' => ':count unique values',
|
'tags_x_unique_values' => ':count 个不重复项目',
|
||||||
'tags_all_values' => 'All values',
|
'tags_all_values' => '所有值',
|
||||||
'tags_view_tags' => 'View Tags',
|
'tags_view_tags' => '查看标签',
|
||||||
'tags_view_existing_tags' => 'View existing tags',
|
'tags_view_existing_tags' => '查看已有的标签',
|
||||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
'tags_list_empty_hint' => '您可以在页面编辑器的侧边栏添加标签,或者在编辑图书、章节、书架时添加。',
|
||||||
'attachments' => '附件',
|
'attachments' => '附件',
|
||||||
'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。',
|
'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。',
|
||||||
'attachments_explain_instant_save' => '这里的更改将立即保存。',
|
'attachments_explain_instant_save' => '这里的更改将立即保存。',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ return [
|
|||||||
'app_custom_html_desc' => '此处添加的任何内容都将插入到每个页面的<head>部分的底部,这对于覆盖样式或添加分析代码很方便。',
|
'app_custom_html_desc' => '此处添加的任何内容都将插入到每个页面的<head>部分的底部,这对于覆盖样式或添加分析代码很方便。',
|
||||||
'app_custom_html_disabled_notice' => '在此设置页面上禁用了自定义HTML标题内容,以确保可以恢复所有重大更改。',
|
'app_custom_html_disabled_notice' => '在此设置页面上禁用了自定义HTML标题内容,以确保可以恢复所有重大更改。',
|
||||||
'app_logo' => '站点Logo',
|
'app_logo' => '站点Logo',
|
||||||
'app_logo_desc' => '这个图片的高度应该为43px。<br>大图片将会被缩小。',
|
'app_logo_desc' => '这个图片的高度应为 43 像素。<br>大图片将会被缩小。',
|
||||||
'app_primary_color' => '站点主色',
|
'app_primary_color' => '站点主色',
|
||||||
'app_primary_color_desc' => '这应该是一个十六进制值。<br>保留为空以重置为默认颜色。',
|
'app_primary_color_desc' => '这应该是一个十六进制值。<br>保留为空以重置为默认颜色。',
|
||||||
'app_homepage' => '站点主页',
|
'app_homepage' => '站点主页',
|
||||||
@@ -151,7 +151,7 @@ return [
|
|||||||
'role_manage_settings' => '管理App设置',
|
'role_manage_settings' => '管理App设置',
|
||||||
'role_export_content' => '导出内容',
|
'role_export_content' => '导出内容',
|
||||||
'role_asset' => '资源许可',
|
'role_asset' => '资源许可',
|
||||||
'roles_system_warning' => '请注意,具有上述三个权限中的任何一个都可以允许用户更改自己的特权或系统中其他人的特权。 只将具有这些权限的角色分配给受信任的用户。',
|
'roles_system_warning' => '请注意,拥有上述三个权限中的任何一个都可以允许用户更改自己的权限或系统中其他人的权限。 请只将拥有这些权限的角色分配给你信任的用户。',
|
||||||
'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍,章节和页面上的权限将覆盖这里的权限设定。',
|
'role_asset_desc' => '对系统内资源的默认访问许可将由这些权限控制。单独设置在书籍,章节和页面上的权限将覆盖这里的权限设定。',
|
||||||
'role_asset_admins' => '管理员可自动获得对所有内容的访问权限,但这些选项可能会显示或隐藏UI选项。',
|
'role_asset_admins' => '管理员可自动获得对所有内容的访问权限,但这些选项可能会显示或隐藏UI选项。',
|
||||||
'role_all' => '全部的',
|
'role_all' => '全部的',
|
||||||
@@ -193,7 +193,7 @@ return [
|
|||||||
'users_edit_profile' => '编辑资料',
|
'users_edit_profile' => '编辑资料',
|
||||||
'users_edit_success' => '用户更新成功',
|
'users_edit_success' => '用户更新成功',
|
||||||
'users_avatar' => '用户头像',
|
'users_avatar' => '用户头像',
|
||||||
'users_avatar_desc' => '当前图片应该为约256px的正方形。',
|
'users_avatar_desc' => '选择一张头像。 这张图片应该是约 256 像素的正方形。',
|
||||||
'users_preferred_language' => '语言',
|
'users_preferred_language' => '语言',
|
||||||
'users_preferred_language_desc' => '此选项将更改用于应用程序用户界面的语言。 这不会影响任何用户创建的内容。',
|
'users_preferred_language_desc' => '此选项将更改用于应用程序用户界面的语言。 这不会影响任何用户创建的内容。',
|
||||||
'users_social_accounts' => '社交账户',
|
'users_social_accounts' => '社交账户',
|
||||||
|
|||||||
@@ -87,6 +87,20 @@
|
|||||||
.card-title a {
|
.card-title a {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
.card-footer-link {
|
||||||
|
display: block;
|
||||||
|
padding: $-s $-m;
|
||||||
|
line-height: 1;
|
||||||
|
border-top: 1px solid;
|
||||||
|
@include lightDark(border-color, #DDD, #555);
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: $-xs;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
@include lightDark(background-color, #f2f2f2, #2d2d2d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card.border-card {
|
.card.border-card {
|
||||||
border: 1px solid #DDD;
|
border: 1px solid #DDD;
|
||||||
@@ -229,6 +243,9 @@
|
|||||||
&:hover, &:focus-within {
|
&:hover, &:focus-within {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ header .search-box {
|
|||||||
&:hover, &:focus-within {
|
&:hover, &:focus-within {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include smaller-than($l) {
|
@include smaller-than($l) {
|
||||||
|
|||||||
@@ -369,6 +369,9 @@ body.flexbox {
|
|||||||
&:focus-within {
|
&:focus-within {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,4 +412,7 @@ body.mce-fullscreen, body.markdown-fullscreen {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,27 +44,27 @@
|
|||||||
<div>
|
<div>
|
||||||
@if(count($favourites) > 0)
|
@if(count($favourites) > 0)
|
||||||
<div id="top-favourites" class="card mb-xl">
|
<div id="top-favourites" class="card mb-xl">
|
||||||
<h3 class="card-title">
|
<h3 class="card-title">{{ trans('entities.my_most_viewed_favourites') }}</h3>
|
||||||
<a href="{{ url('/favourites') }}" class="no-color">{{ trans('entities.my_most_viewed_favourites') }}</a>
|
|
||||||
</h3>
|
|
||||||
<div class="px-m">
|
<div class="px-m">
|
||||||
@include('entities.list', [
|
@include('entities.list', [
|
||||||
'entities' => $favourites,
|
'entities' => $favourites,
|
||||||
'style' => 'compact',
|
'style' => 'compact',
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{{ url('/favourites') }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div id="recent-pages" class="card mb-xl">
|
<div id="recent-pages" class="card mb-xl">
|
||||||
<h3 class="card-title"><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
|
<h3 class="card-title">{{ trans('entities.recently_updated_pages') }}</h3>
|
||||||
<div id="recently-updated-pages" class="px-m">
|
<div id="recently-updated-pages" class="px-m">
|
||||||
@include('entities.list', [
|
@include('entities.list', [
|
||||||
'entities' => $recentlyUpdatedPages,
|
'entities' => $recentlyUpdatedPages,
|
||||||
'style' => 'compact',
|
'style' => 'compact',
|
||||||
'emptyText' => trans('entities.no_pages_recently_updated')
|
'emptyText' => trans('entities.no_pages_recently_updated'),
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{{ url("/pages/recently-updated") }}" class="card-footer-link">{{ trans('common.view_all') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,12 @@
|
|||||||
|
|
||||||
@if(count($favourites) > 0)
|
@if(count($favourites) > 0)
|
||||||
<div id="top-favourites" class="mb-xl">
|
<div id="top-favourites" class="mb-xl">
|
||||||
<h5>
|
<h5>{{ trans('entities.my_most_viewed_favourites') }}</h5>
|
||||||
<a href="{{ url('/favourites') }}" class="no-color">{{ trans('entities.my_most_viewed_favourites') }}</a>
|
|
||||||
</h5>
|
|
||||||
@include('entities.list', [
|
@include('entities.list', [
|
||||||
'entities' => $favourites,
|
'entities' => $favourites,
|
||||||
'style' => 'compact',
|
'style' => 'compact',
|
||||||
])
|
])
|
||||||
|
<a href="{{ url('/favourites') }}" class="text-muted block py-xs">{{ trans('common.view_all') }}</a>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-xl">
|
<div class="mb-xl">
|
||||||
<h5><a class="no-color" href="{{ url("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h5>
|
<h5>{{ trans('entities.recently_updated_pages') }}</h5>
|
||||||
<div id="recently-updated-pages">
|
<div id="recently-updated-pages">
|
||||||
@include('entities.list', [
|
@include('entities.list', [
|
||||||
'entities' => $recentlyUpdatedPages,
|
'entities' => $recentlyUpdatedPages,
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
'emptyText' => trans('entities.no_pages_recently_updated')
|
'emptyText' => trans('entities.no_pages_recently_updated')
|
||||||
])
|
])
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{{ url('/pages/recently-updated') }}" class="text-muted block py-xs">{{ trans('common.view_all') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="recent-activity" class="mb-xl">
|
<div id="recent-activity" class="mb-xl">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="code"
|
name="code"
|
||||||
|
autofocus
|
||||||
placeholder="{{ trans('auth.mfa_gen_totp_provide_code_here') }}"
|
placeholder="{{ trans('auth.mfa_gen_totp_provide_code_here') }}"
|
||||||
class="input-fill-width {{ $errors->has('code') ? 'neg' : '' }}">
|
class="input-fill-width {{ $errors->has('code') ? 'neg' : '' }}">
|
||||||
@if($errors->has('code'))
|
@if($errors->has('code'))
|
||||||
@@ -14,4 +15,4 @@
|
|||||||
<div class="mt-s text-right">
|
<div class="mt-s text-right">
|
||||||
<button class="button">{{ trans('common.confirm') }}</button>
|
<button class="button">{{ trans('common.confirm') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,17 +12,19 @@
|
|||||||
|
|
||||||
<p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
|
<p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
|
||||||
|
|
||||||
<hr class="my-l">
|
@if(userCan('users-manage'))
|
||||||
|
<hr class="my-l">
|
||||||
|
|
||||||
<div class="grid half gap-xl v-center">
|
<div class="grid half gap-xl v-center">
|
||||||
<div>
|
<div>
|
||||||
<label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
|
<label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
|
||||||
<p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
|
<p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@include('form.user-select', ['name' => 'new_owner_id', 'user' => null, 'compact' => false])
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
@endif
|
||||||
@include('form.user-select', ['name' => 'new_owner_id', 'user' => null, 'compact' => false])
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-l">
|
<hr class="my-l">
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Tests\Api;
|
namespace Tests\Api;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\RolePermission;
|
use BookStack\Auth\Permissions\RolePermission;
|
||||||
|
use BookStack\Auth\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@@ -91,6 +92,26 @@ class ApiAuthTest extends TestCase
|
|||||||
$resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));
|
$resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_access_prevented_for_guest_users_with_api_permission_while_public_access_disabled()
|
||||||
|
{
|
||||||
|
$this->disableCookieEncryption();
|
||||||
|
$publicRole = Role::getSystemRole('public');
|
||||||
|
$accessApiPermission = RolePermission::getByName('access-api');
|
||||||
|
$publicRole->attachPermission($accessApiPermission);
|
||||||
|
|
||||||
|
$this->withCookie('bookstack_session', 'abc123');
|
||||||
|
|
||||||
|
// Test API access when not public
|
||||||
|
setting()->put('app-public', false);
|
||||||
|
$resp = $this->get($this->endpoint);
|
||||||
|
$resp->assertStatus(403);
|
||||||
|
|
||||||
|
// Test API access when public
|
||||||
|
setting()->put('app-public', true);
|
||||||
|
$resp = $this->get($this->endpoint);
|
||||||
|
$resp->assertStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_token_expiry_checked()
|
public function test_token_expiry_checked()
|
||||||
{
|
{
|
||||||
$editor = $this->getEditor();
|
$editor = $this->getEditor();
|
||||||
|
|||||||
@@ -224,6 +224,29 @@ class AttachmentsApiTest extends TestCase
|
|||||||
unlink(storage_path($attachment->path));
|
unlink(storage_path($attachment->path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_attachment_not_visible_on_other_users_draft()
|
||||||
|
{
|
||||||
|
$this->actingAsApiAdmin();
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = Page::query()->first();
|
||||||
|
$page->draft = true;
|
||||||
|
$page->owned_by = $editor;
|
||||||
|
$page->save();
|
||||||
|
$this->regenEntityPermissions($page);
|
||||||
|
|
||||||
|
$attachment = $this->createAttachmentForPage($page, [
|
||||||
|
'name' => 'my attachment',
|
||||||
|
'path' => 'https://example.com',
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resp = $this->getJson("{$this->baseEndpoint}/{$attachment->id}");
|
||||||
|
|
||||||
|
$resp->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_update_endpoint()
|
public function test_update_endpoint()
|
||||||
{
|
{
|
||||||
$this->actingAsApiAdmin();
|
$this->actingAsApiAdmin();
|
||||||
|
|||||||
@@ -36,6 +36,38 @@ class SearchApiTest extends TestCase
|
|||||||
$resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'bookshelf']);
|
$resp->assertJsonFragment(['name' => $uniqueTerm, 'type' => 'bookshelf']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_all_endpoint_returns_entity_url()
|
||||||
|
{
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = Page::query()->first();
|
||||||
|
$page->update(['name' => 'name with superuniquevalue within']);
|
||||||
|
$page->indexForSearch();
|
||||||
|
|
||||||
|
$resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');
|
||||||
|
$resp->assertJsonFragment([
|
||||||
|
'type' => 'page',
|
||||||
|
'url' => $page->getUrl(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_all_endpoint_returns_items_with_preview_html()
|
||||||
|
{
|
||||||
|
/** @var Book $book */
|
||||||
|
$book = Book::query()->first();
|
||||||
|
$book->update(['name' => 'name with superuniquevalue within', 'description' => 'Description with superuniquevalue within']);
|
||||||
|
$book->indexForSearch();
|
||||||
|
|
||||||
|
$resp = $this->actingAsApiAdmin()->getJson($this->baseEndpoint . '?query=superuniquevalue');
|
||||||
|
$resp->assertJsonFragment([
|
||||||
|
'type' => 'book',
|
||||||
|
'url' => $book->getUrl(),
|
||||||
|
'preview_html' => [
|
||||||
|
'name' => 'name with <strong>superuniquevalue</strong> within',
|
||||||
|
'content' => 'Description with <strong>superuniquevalue</strong> within',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_all_endpoint_requires_query_parameter()
|
public function test_all_endpoint_requires_query_parameter()
|
||||||
{
|
{
|
||||||
$resp = $this->actingAsApiEditor()->get($this->baseEndpoint);
|
$resp = $this->actingAsApiEditor()->get($this->baseEndpoint);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class MfaVerificationTest extends TestCase
|
|||||||
$resp = $this->get('/mfa/verify');
|
$resp = $this->get('/mfa/verify');
|
||||||
$resp->assertSee('Verify Access');
|
$resp->assertSee('Verify Access');
|
||||||
$resp->assertSee('Enter the code, generated using your mobile app, below:');
|
$resp->assertSee('Enter the code, generated using your mobile app, below:');
|
||||||
$resp->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"]');
|
$resp->assertElementExists('form[action$="/mfa/totp/verify"] input[name="code"][autofocus]');
|
||||||
|
|
||||||
$google2fa = new Google2FA();
|
$google2fa = new Google2FA();
|
||||||
$resp = $this->post('/mfa/totp/verify', [
|
$resp = $this->post('/mfa/totp/verify', [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use BookStack\Auth\Role;
|
|||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Entities\Tools\PdfGenerator;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@@ -289,6 +290,24 @@ class ExportTest extends TestCase
|
|||||||
$resp->assertDontSee('ExportWizardTheFifth');
|
$resp->assertDontSee('ExportWizardTheFifth');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_page_pdf_export_converts_iframes_to_links()
|
||||||
|
{
|
||||||
|
$page = Page::query()->first()->forceFill([
|
||||||
|
'html' => '<iframe width="560" height="315" src="//www.youtube.com/embed/ShqUjt33uOs"></iframe>',
|
||||||
|
]);
|
||||||
|
$page->save();
|
||||||
|
|
||||||
|
$pdfHtml = '';
|
||||||
|
$mockPdfGenerator = $this->mock(PdfGenerator::class);
|
||||||
|
$mockPdfGenerator->shouldReceive('fromHtml')
|
||||||
|
->with(\Mockery::capture($pdfHtml))
|
||||||
|
->andReturn('');
|
||||||
|
|
||||||
|
$this->asEditor()->get($page->getUrl('/export/pdf'));
|
||||||
|
$this->assertStringNotContainsString('iframe>', $pdfHtml);
|
||||||
|
$this->assertStringContainsString('<p><a href="https://www.youtube.com/embed/ShqUjt33uOs">https://www.youtube.com/embed/ShqUjt33uOs</a></p>', $pdfHtml);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_page_markdown_export()
|
public function test_page_markdown_export()
|
||||||
{
|
{
|
||||||
$page = Page::query()->first();
|
$page = Page::query()->first();
|
||||||
|
|||||||
@@ -670,4 +670,24 @@ class PageContentTest extends TestCase
|
|||||||
$page->refresh();
|
$page->refresh();
|
||||||
$this->assertStringContainsString('<img src=""', $page->html);
|
$this->assertStringContainsString('<img src=""', $page->html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_nested_headers_gets_assigned_an_id()
|
||||||
|
{
|
||||||
|
$this->asEditor();
|
||||||
|
$page = Page::query()->first();
|
||||||
|
|
||||||
|
$content = '<table><tbody><tr><td><h5>Simple Test</h5></td></tr></tbody></table>';
|
||||||
|
$this->put($page->getUrl(), [
|
||||||
|
'name' => $page->name,
|
||||||
|
'html' => $content,
|
||||||
|
'summary' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$updatedPage = Page::query()->where('id', '=', $page->id)->first();
|
||||||
|
|
||||||
|
// The top level <table> node will get assign the bkmrk-simple-test id because the system will
|
||||||
|
// take the node value of h5
|
||||||
|
// So the h5 should get the bkmrk-simple-test-1 id
|
||||||
|
$this->assertStringContainsString('<h5 id="bkmrk-simple-test-1">Simple Test</h5>', $updatedPage->html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ use BookStack\Entities\Models\Page;
|
|||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use League\CommonMark\ConfigurableEnvironmentInterface;
|
use League\CommonMark\ConfigurableEnvironmentInterface;
|
||||||
|
|
||||||
@@ -206,6 +208,16 @@ class ThemeTest extends TestCase
|
|||||||
$this->assertStringContainsString('donkey=donut', $redirect);
|
$this->assertStringContainsString('donkey=donut', $redirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_register_command_allows_provided_command_to_be_usable_via_artisan()
|
||||||
|
{
|
||||||
|
Theme::registerCommand(new MyCustomCommand());
|
||||||
|
|
||||||
|
Artisan::call('bookstack:test-custom-command', []);
|
||||||
|
$output = Artisan::output();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('Command ran!', $output);
|
||||||
|
}
|
||||||
|
|
||||||
protected function usingThemeFolder(callable $callback)
|
protected function usingThemeFolder(callable $callback)
|
||||||
{
|
{
|
||||||
// Create a folder and configure a theme
|
// Create a folder and configure a theme
|
||||||
@@ -220,3 +232,13 @@ class ThemeTest extends TestCase
|
|||||||
File::deleteDirectory($themeFolderPath);
|
File::deleteDirectory($themeFolderPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MyCustomCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'bookstack:test-custom-command';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->line('Command ran!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
31
tests/Unit/FrameworkAssumptionTest.php
Normal file
31
tests/Unit/FrameworkAssumptionTest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use BadMethodCallException;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class tests assumptions we're relying upon in the framework.
|
||||||
|
* This is primarily to keep track of certain bits of functionality that
|
||||||
|
* may be used in important areas such as to enforce permissions.
|
||||||
|
*/
|
||||||
|
class FrameworkAssumptionTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_scopes_error_if_not_existing()
|
||||||
|
{
|
||||||
|
$this->expectException(BadMethodCallException::class);
|
||||||
|
$this->expectExceptionMessage('Call to undefined method BookStack\Entities\Models\Page::scopeNotfoundscope()');
|
||||||
|
Page::query()->scopes('notfoundscope');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scopes_applies_upon_existing()
|
||||||
|
{
|
||||||
|
// Page has SoftDeletes trait by default, so we apply our custom scope and ensure
|
||||||
|
// it stacks on the global scope to filter out deleted items.
|
||||||
|
$query = Page::query()->scopes('visible')->toSql();
|
||||||
|
$this->assertStringContainsString('joint_permissions', $query);
|
||||||
|
$this->assertStringContainsString('`deleted_at` is null', $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,6 +130,21 @@ class UserManagementTest extends TestCase
|
|||||||
$resp->assertSee('new_owner_id');
|
$resp->assertSee('new_owner_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_migrate_option_hidden_if_user_cannot_manage_users()
|
||||||
|
{
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
|
||||||
|
$resp = $this->asEditor()->get("settings/users/{$editor->id}/delete");
|
||||||
|
$resp->assertDontSee('Migrate Ownership');
|
||||||
|
$resp->assertDontSee('new_owner_id');
|
||||||
|
|
||||||
|
$this->giveUserPermissions($editor, ['users-manage']);
|
||||||
|
|
||||||
|
$resp = $this->asEditor()->get("settings/users/{$editor->id}/delete");
|
||||||
|
$resp->assertSee('Migrate Ownership');
|
||||||
|
$resp->assertSee('new_owner_id');
|
||||||
|
}
|
||||||
|
|
||||||
public function test_delete_with_new_owner_id_changes_ownership()
|
public function test_delete_with_new_owner_id_changes_ownership()
|
||||||
{
|
{
|
||||||
$page = Page::query()->first();
|
$page = Page::query()->first();
|
||||||
|
|||||||
66
tests/User/UserSearchTest.php
Normal file
66
tests/User/UserSearchTest.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\User;
|
||||||
|
|
||||||
|
use BookStack\Auth\User;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class UserSearchTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_select_search_matches_by_name()
|
||||||
|
{
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
$admin = $this->getAdmin();
|
||||||
|
$resp = $this->actingAs($admin)->get('/search/users/select?search=' . urlencode($viewer->name));
|
||||||
|
|
||||||
|
$resp->assertOk();
|
||||||
|
$resp->assertSee($viewer->name);
|
||||||
|
$resp->assertDontSee($admin->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_select_search_shows_first_by_name_without_search()
|
||||||
|
{
|
||||||
|
/** @var User $firstUser */
|
||||||
|
$firstUser = User::query()->orderBy('name', 'desc')->first();
|
||||||
|
$resp = $this->asAdmin()->get('/search/users/select');
|
||||||
|
|
||||||
|
$resp->assertOk();
|
||||||
|
$resp->assertSee($firstUser->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_select_search_does_not_match_by_email()
|
||||||
|
{
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
$editor = $this->getEditor();
|
||||||
|
$resp = $this->actingAs($editor)->get('/search/users/select?search=' . urlencode($viewer->email));
|
||||||
|
|
||||||
|
$resp->assertDontSee($viewer->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_select_requires_right_permission()
|
||||||
|
{
|
||||||
|
$permissions = ['users-manage', 'restrictions-manage-own', 'restrictions-manage-all'];
|
||||||
|
$user = $this->getViewer();
|
||||||
|
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
$resp = $this->actingAs($user)->get('/search/users/select?search=a');
|
||||||
|
$this->assertPermissionError($resp);
|
||||||
|
|
||||||
|
$this->giveUserPermissions($user, [$permission]);
|
||||||
|
$resp = $this->actingAs($user)->get('/search/users/select?search=a');
|
||||||
|
$resp->assertOk();
|
||||||
|
$user->roles()->delete();
|
||||||
|
$user->clearPermissionCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_select_requires_logged_in_user()
|
||||||
|
{
|
||||||
|
$this->setSettings(['app-public' => true]);
|
||||||
|
$defaultUser = User::getDefault();
|
||||||
|
$this->giveUserPermissions($defaultUser, ['users-manage']);
|
||||||
|
|
||||||
|
$resp = $this->get('/search/users/select?search=a');
|
||||||
|
$this->assertPermissionError($resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user