mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-16 13:43:48 +03:00
Compare commits
23 Commits
phpstan_ju
...
l10n_devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b8cb2c8c1 | ||
|
|
6107161275 | ||
|
|
ad283ef0ed | ||
|
|
b87789dde6 | ||
|
|
a213175004 | ||
|
|
b6e3d304fe | ||
|
|
6bba39196c | ||
|
|
caeea658d1 | ||
|
|
fe39b69c1f | ||
|
|
01dc1e71c5 | ||
|
|
59bbf504cf | ||
|
|
f7df78b91b | ||
|
|
c511f7d935 | ||
|
|
79a2e017bb | ||
|
|
dad83d473d | ||
|
|
c74df7e06b | ||
|
|
6b545d600c | ||
|
|
cc0b059fa4 | ||
|
|
9fc46f76f6 | ||
|
|
84a23fb23f | ||
|
|
b7325fdf0e | ||
|
|
81f77a95de | ||
|
|
37f2d05118 |
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: https://github.com/shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.5
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
extensions: gd, mbstring, json, curl, xml, dom, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
|
||||
@@ -23,6 +23,14 @@ jobs:
|
||||
services:
|
||||
mysql:
|
||||
image: docker.io/library/mariadb:12.2.2-noble
|
||||
options: --tmpfs /var/lib/mysql:rw
|
||||
cmd:
|
||||
- --innodb-flush-log-at-trx-commit=0
|
||||
- --innodb-flush-method=O_DIRECT
|
||||
- --innodb-doublewrite=0
|
||||
- --innodb-buffer-pool-size=256M
|
||||
- --skip-log-bin
|
||||
- --sync-binlog=0
|
||||
env:
|
||||
MARIADB_USER: bookstack-test
|
||||
MARIADB_PASSWORD: bookstack-test
|
||||
@@ -35,7 +43,7 @@ jobs:
|
||||
uses: https://github.com/shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
extensions: gd, mbstring, json, curl, xml, dom, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
matrix:
|
||||
php: ['8.2', '8.3', '8.4', '8.5']
|
||||
env:
|
||||
phpextensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
|
||||
phpextensions: gd, mbstring, json, curl, xml, dom, mysql, ldap, gmp
|
||||
phpextensioncachekey: cache-v1
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
33
.forgejo/workflows/update-snyk.yml
Normal file
33
.forgejo/workflows/update-snyk.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: update-snyk
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'composer*'
|
||||
- 'package*'
|
||||
branches:
|
||||
- 'development'
|
||||
- 'release'
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: docker.io/library/node:24-trixie
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v6
|
||||
|
||||
- name: Update Snyk for monitoring - Composer
|
||||
uses: https://github.com/snyk/actions/node@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: snyk monitor --file=composer.lock --project-name=bookstack-${{forgejo.ref_name}}-composer
|
||||
|
||||
- name: Update Snyk for monitoring - NPM
|
||||
uses: https://github.com/snyk/actions/node@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: snyk monitor --file=package-lock.json --project-name=bookstack-${{forgejo.ref_name}}-npm
|
||||
4
.github/translators.txt
vendored
4
.github/translators.txt
vendored
@@ -544,3 +544,7 @@ FelixFrizzy :: German
|
||||
Pedro de Mattia (pdmtt) :: Portuguese, Brazilian
|
||||
lonestan :: Russian
|
||||
Paul Kernstock (kernstock) :: German
|
||||
brtbr :: German; German Informal
|
||||
Ricardo Covelo (covelo12) :: Portuguese
|
||||
Bojan Maksimovic (PolarniMeda) :: Serbian (Cyrillic)
|
||||
Dian Prawira (wiradian84) :: Indonesian
|
||||
|
||||
@@ -8,6 +8,7 @@ use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Util\UrlComparison;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -186,7 +187,8 @@ class LoginController extends Controller
|
||||
{
|
||||
// Store the previous location for redirect after login
|
||||
$previous = url()->previous('');
|
||||
$isPreviousFromInstance = str_starts_with($previous, url('/'));
|
||||
$comparison = new UrlComparison($previous, url('/'));
|
||||
$isPreviousFromInstance = $comparison->originsMatch() && $comparison->pathsOverlap();
|
||||
if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
/**
|
||||
* Retrieve a user by the given credentials.
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials): ?User
|
||||
public function retrieveByCredentials(array $credentials): ?Authenticatable
|
||||
{
|
||||
return User::query()
|
||||
->where('external_auth_id', $credentials['external_auth_id'])
|
||||
|
||||
@@ -50,12 +50,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Create a new authentication guard.
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
UserProvider $provider,
|
||||
Session $session,
|
||||
RegistrationService $registrationService
|
||||
) {
|
||||
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->session = $session;
|
||||
$this->provider = $provider;
|
||||
|
||||
@@ -83,10 +83,6 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
}
|
||||
}
|
||||
|
||||
if (!($user instanceof User)) {
|
||||
throw new LoginAttemptException('Could not find or create a user for LDAP login.');
|
||||
}
|
||||
|
||||
// Sync LDAP groups if required
|
||||
if ($this->ldapService->shouldSyncGroups()) {
|
||||
$this->ldapService->syncGroups($user, $username);
|
||||
|
||||
@@ -131,6 +131,8 @@ class OidcJwtWithClaims implements ProvidesClaims
|
||||
}
|
||||
}, $this->keys);
|
||||
|
||||
$parsedKeys = array_filter($parsedKeys);
|
||||
|
||||
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
|
||||
/** @var OidcJwtSigningKey $parsedKey */
|
||||
foreach ($parsedKeys as $parsedKey) {
|
||||
|
||||
@@ -80,7 +80,6 @@ class OidcService
|
||||
$provider->setPkceCode($pkceCode);
|
||||
|
||||
// Try to exchange authorization code for access token
|
||||
/** @var OidcAccessToken $accessToken */
|
||||
$accessToken = $provider->getAccessToken('authorization_code', [
|
||||
'code' => $authorizationCode,
|
||||
]);
|
||||
|
||||
@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use BookStack\Users\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
|
||||
@@ -28,7 +27,6 @@ class ActivityQueries
|
||||
*/
|
||||
public function latest(int $count = 20, int $page = 0): array
|
||||
{
|
||||
/** @var Collection<int, Activity> $activityList */
|
||||
$activityList = $this->permissions
|
||||
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
@@ -85,7 +83,6 @@ class ActivityQueries
|
||||
*/
|
||||
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||
{
|
||||
/** @var Collection<int, Activity> $activityList */
|
||||
$activityList = $this->permissions
|
||||
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
|
||||
@@ -59,8 +59,7 @@ class CommentController extends Controller
|
||||
'html' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
$comment = $this->commentRepo->getById($commentId);
|
||||
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
|
||||
$comment = $this->commentRepo->getVisibleById($commentId);
|
||||
$this->checkOwnablePermission(Permission::CommentUpdate, $comment);
|
||||
|
||||
$comment = $this->commentRepo->update($comment, $input['html']);
|
||||
@@ -76,8 +75,7 @@ class CommentController extends Controller
|
||||
*/
|
||||
public function archive(int $id)
|
||||
{
|
||||
$comment = $this->commentRepo->getById($id);
|
||||
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
|
||||
$comment = $this->commentRepo->getVisibleById($id);
|
||||
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
@@ -96,8 +94,7 @@ class CommentController extends Controller
|
||||
*/
|
||||
public function unarchive(int $id)
|
||||
{
|
||||
$comment = $this->commentRepo->getById($id);
|
||||
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
|
||||
$comment = $this->commentRepo->getVisibleById($id);
|
||||
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
@@ -116,7 +113,7 @@ class CommentController extends Controller
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$comment = $this->commentRepo->getById($id);
|
||||
$comment = $this->commentRepo->getVisibleById($id);
|
||||
$this->checkOwnablePermission(Permission::CommentDelete, $comment);
|
||||
|
||||
$this->commentRepo->delete($comment);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Activity\Models;
|
||||
|
||||
use BookStack\App\Model;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Permissions\Models\JointPermission;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||
@@ -41,9 +40,6 @@ class Comment extends Model implements Loggable, OwnableInterface
|
||||
|
||||
/**
|
||||
* Get the entity that this comment belongs to.
|
||||
* It's only pages right now hence the typing below.
|
||||
* Would need a deeper audit if that changes as many areas assume this is always a page.
|
||||
* @return MorphTo<Page, $this>
|
||||
*/
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
@@ -59,9 +55,7 @@ class Comment extends Model implements Loggable, OwnableInterface
|
||||
|
||||
// Ultimately, we could just align the method name to 'commentable' but that would be a potential
|
||||
// breaking change and not really worthwhile in a patch due to the risk of creating extra problems.
|
||||
/** @var MorphTo<Page, $this> $relation */
|
||||
$relation = $this->morphTo(null, 'commentable_type', 'commentable_id');
|
||||
return $relation;
|
||||
return $this->morphTo(null, 'commentable_type', 'commentable_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,17 +73,13 @@ class WebhookFormatter
|
||||
// Load entity owner, creator, updater details
|
||||
$this->addModelFormatter(
|
||||
fn ($event, $model) => ($model instanceof Entity),
|
||||
function ($model) {
|
||||
$model->load(['ownedBy', 'createdBy', 'updatedBy']);
|
||||
}
|
||||
fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy'])
|
||||
);
|
||||
|
||||
// Load revision detail for page update and create events
|
||||
$this->addModelFormatter(
|
||||
fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),
|
||||
function ($model) {
|
||||
$model->load('currentRevision');
|
||||
}
|
||||
fn ($model) => $model->load('currentRevision')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ class ApiDocsGenerator
|
||||
return $validation;
|
||||
}
|
||||
|
||||
if (is_object($validation) && $validation instanceof \Stringable) {
|
||||
return $validation->__toString();
|
||||
if (is_object($validation) && method_exists($validation, '__toString')) {
|
||||
return strval($validation);
|
||||
}
|
||||
|
||||
if ($validation instanceof Password) {
|
||||
|
||||
@@ -7,7 +7,6 @@ use BookStack\Theming\ThemeService;
|
||||
use BookStack\Theming\ThemeViews;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\View\FileViewFinder;
|
||||
|
||||
class ThemeServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -28,11 +27,7 @@ class ThemeServiceProvider extends ServiceProvider
|
||||
// Boot up the theme system
|
||||
$themeService = $this->app->make(ThemeService::class);
|
||||
$viewFactory = $this->app->make('view');
|
||||
$viewFinder = $viewFactory->getFinder();
|
||||
if (!($viewFinder instanceof FileViewFinder)) {
|
||||
throw new \Exception('Only the file view finder is supported for the theme system');
|
||||
}
|
||||
$themeViews = new ThemeViews($viewFinder);
|
||||
$themeViews = new ThemeViews($viewFactory->getFinder());
|
||||
|
||||
// Use a custom include so that we can insert theme views before/after includes.
|
||||
// This is done, even if no theme is active, so that view caching does not create problems
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\App\Providers;
|
||||
|
||||
use BookStack\Uploads\ImageService;
|
||||
use BookStack\Util\UrlFilter;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@@ -21,10 +22,8 @@ class ValidationRuleServiceProvider extends ServiceProvider
|
||||
|
||||
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
|
||||
$cleanLinkName = strtolower(trim($value));
|
||||
$isJs = str_starts_with($cleanLinkName, 'javascript:');
|
||||
$isData = str_starts_with($cleanLinkName, 'data:');
|
||||
|
||||
return !$isJs && !$isData;
|
||||
$filter = new UrlFilter($cleanLinkName);
|
||||
return $filter->isAllowed();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use BookStack\Facades\Theme;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use BookStack\Settings\SettingService;
|
||||
use BookStack\Users\Models\OwnableInterface;
|
||||
use BookStack\Users\Models\User;
|
||||
|
||||
/**
|
||||
@@ -41,7 +40,7 @@ function user(): User
|
||||
* Check if the current user has a permission. If an ownable element
|
||||
* is passed in the jointPermissions are checked against that particular item.
|
||||
*/
|
||||
function userCan(string|Permission $permission, (Model&OwnableInterface)|null $ownable = null): bool
|
||||
function userCan(string|Permission $permission, ?Model $ownable = null): bool
|
||||
{
|
||||
if (is_null($ownable)) {
|
||||
return user()->can($permission);
|
||||
|
||||
@@ -7,6 +7,7 @@ use BookStack\Theming\ThemeModule;
|
||||
use BookStack\Theming\ThemeModuleException;
|
||||
use BookStack\Theming\ThemeModuleManager;
|
||||
use BookStack\Theming\ThemeModuleZip;
|
||||
use BookStack\Util\UrlComparison;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -199,7 +200,6 @@ class InstallModuleCommand extends Command
|
||||
{
|
||||
$httpRequests = app()->make(HttpRequestService::class);
|
||||
$client = $httpRequests->buildClient(30, ['stream' => true]);
|
||||
$originalUrl = parse_url($location);
|
||||
$currentLocation = $location;
|
||||
$maxRedirects = 3;
|
||||
$redirectCount = 0;
|
||||
@@ -212,12 +212,11 @@ class InstallModuleCommand extends Command
|
||||
if ($statusCode >= 300 && $statusCode < 400 && $redirectCount < $maxRedirects) {
|
||||
$redirectLocation = $resp->getHeaderLine('Location');
|
||||
if ($redirectLocation) {
|
||||
$redirectUrl = parse_url($redirectLocation);
|
||||
$redirectOriginMatches = ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')
|
||||
&& ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '')
|
||||
&& ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '');
|
||||
$comparison = new UrlComparison($location, $redirectLocation);
|
||||
$redirectOriginMatches = $comparison->originsMatch();
|
||||
|
||||
if (!$redirectOriginMatches) {
|
||||
$redirectUrl = parse_url($redirectLocation);
|
||||
$redirectOrigin = ($redirectUrl['scheme'] ?? '') . '://' . ($redirectUrl['host'] ?? '') . (isset($redirectUrl['port']) ? ':' . $redirectUrl['port'] : '');
|
||||
$this->info("The download URL is redirecting to a different site: {$redirectOrigin}");
|
||||
$shouldContinue = $this->confirm("Do you trust downloading the module from this site?");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BookStack\Entities\Controllers;
|
||||
|
||||
use BookStack\Activity\Tools\CommentTree;
|
||||
@@ -12,9 +10,7 @@ use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Http\ApiController;
|
||||
use BookStack\Permissions\Permission;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class PageApiController extends ApiController
|
||||
{
|
||||
@@ -49,7 +45,7 @@ class PageApiController extends ApiController
|
||||
/**
|
||||
* Get a listing of pages visible to the user.
|
||||
*/
|
||||
public function list(): JsonResponse
|
||||
public function list()
|
||||
{
|
||||
$pages = $this->queries->visibleForList()
|
||||
->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
|
||||
@@ -73,7 +69,7 @@ class PageApiController extends ApiController
|
||||
* Any images included via base64 data URIs will be extracted and saved as gallery
|
||||
* images against the page during upload.
|
||||
*/
|
||||
public function create(Request $request): JsonResponse
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->validate($request, $this->rules['create']);
|
||||
|
||||
@@ -106,9 +102,9 @@ class PageApiController extends ApiController
|
||||
* Comments for the page are provided in a tree-structure representing the hierarchy of top-level
|
||||
* comments and replies, for both archived and active comments.
|
||||
*/
|
||||
public function read(string $id): JsonResponse
|
||||
public function read(string $id)
|
||||
{
|
||||
$page = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||
$page = $this->queries->findVisibleByIdOrFail($id);
|
||||
|
||||
$page = $page->forJsonDisplay();
|
||||
$commentTree = (new CommentTree($page));
|
||||
@@ -128,11 +124,11 @@ class PageApiController extends ApiController
|
||||
* Providing a 'book_id' or 'chapter_id' property will essentially move
|
||||
* the page into that parent element if you have permissions to do so.
|
||||
*/
|
||||
public function update(Request $request, string $id): JsonResponse
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$requestData = $this->validate($request, $this->rules['update']);
|
||||
|
||||
$page = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||
$page = $this->queries->findVisibleByIdOrFail($id);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
$parent = null;
|
||||
@@ -165,9 +161,9 @@ class PageApiController extends ApiController
|
||||
* Delete a page.
|
||||
* This will typically send the page to the recycle bin.
|
||||
*/
|
||||
public function delete(string $id): Response
|
||||
public function delete(string $id)
|
||||
{
|
||||
$page = $this->queries->findVisibleByIdOrFail(intval($id));
|
||||
$page = $this->queries->findVisibleByIdOrFail($id);
|
||||
$this->checkOwnablePermission(Permission::PageDelete, $page);
|
||||
|
||||
$this->pageRepo->destroy($page);
|
||||
|
||||
@@ -25,7 +25,6 @@ use BookStack\Util\HtmlContentFilter;
|
||||
use BookStack\Util\HtmlContentFilterConfig;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
@@ -359,8 +358,8 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$visibleBelongsScope = function (Relation $relation): void {
|
||||
$relation->scopes('visible');
|
||||
$visibleBelongsScope = function (BelongsTo $query) {
|
||||
$query->scopes('visible');
|
||||
};
|
||||
|
||||
$pages = $this->queries->visibleForList()
|
||||
|
||||
@@ -177,7 +177,7 @@ class PageRevisionController extends Controller
|
||||
*/
|
||||
public function destroyUserDraft(string $pageId)
|
||||
{
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail(intval($pageId));
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
$this->revisionRepo->deleteDraftsForCurrentUser($page);
|
||||
|
||||
return response('', 200);
|
||||
|
||||
@@ -20,10 +20,10 @@ use Illuminate\Support\Collection;
|
||||
* @property ?int $image_id
|
||||
* @property ?int $default_template_id
|
||||
* @property ?int $sort_rule_id
|
||||
* @property \Illuminate\Database\Eloquent\Collection<int, Chapter> $chapters
|
||||
* @property \Illuminate\Database\Eloquent\Collection<int, Page> $pages
|
||||
* @property \Illuminate\Database\Eloquent\Collection<int, Page> $directPages
|
||||
* @property \Illuminate\Database\Eloquent\Collection<int, Bookshelf> $shelves
|
||||
* @property \Illuminate\Database\Eloquent\Collection $chapters
|
||||
* @property \Illuminate\Database\Eloquent\Collection $pages
|
||||
* @property \Illuminate\Database\Eloquent\Collection $directPages
|
||||
* @property \Illuminate\Database\Eloquent\Collection $shelves
|
||||
* @property ?SortRule $sortRule
|
||||
*/
|
||||
class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface
|
||||
|
||||
@@ -46,7 +46,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int|null $created_by
|
||||
* @property int|null $updated_by
|
||||
* @property int|null $owned_by
|
||||
* @property Collection<int, Tag> $tags
|
||||
* @property Collection $tags
|
||||
*
|
||||
* @method static Entity|Builder visible()
|
||||
* @method static Builder withLastView()
|
||||
|
||||
@@ -24,8 +24,8 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
* @property int $revision_count
|
||||
* @property string $editor
|
||||
* @property Chapter|null $chapter
|
||||
* @property Collection<int, Attachment> $attachments
|
||||
* @property Collection<int, PageRevision> $revisions
|
||||
* @property Collection $attachments
|
||||
* @property Collection $revisions
|
||||
* @property PageRevision $currentRevision
|
||||
*/
|
||||
class Page extends BookChild
|
||||
|
||||
@@ -6,7 +6,6 @@ use BookStack\Activity\Models\View;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Tools\MixedEntityListLoader;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -21,7 +20,6 @@ class QueryPopular
|
||||
|
||||
public function run(int $count, int $page, array $filterModels): Collection
|
||||
{
|
||||
/** @var Builder<View> $query */
|
||||
$query = $this->permissions
|
||||
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace BookStack\Entities\Queries;
|
||||
use BookStack\Activity\Models\View;
|
||||
use BookStack\Entities\Tools\MixedEntityListLoader;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class QueryRecentlyViewed
|
||||
@@ -23,7 +22,6 @@ class QueryRecentlyViewed
|
||||
return collect();
|
||||
}
|
||||
|
||||
/** @var Builder<View> $query */
|
||||
$query = $this->permissions->restrictEntityRelationQuery(
|
||||
View::query(),
|
||||
'views',
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace BookStack\Entities\Queries;
|
||||
use BookStack\Activity\Models\Favourite;
|
||||
use BookStack\Entities\Tools\MixedEntityListLoader;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class QueryTopFavourites
|
||||
@@ -23,7 +22,6 @@ class QueryTopFavourites
|
||||
return collect();
|
||||
}
|
||||
|
||||
/** @var Builder<Favourite> $query */
|
||||
$query = $this->permissions
|
||||
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
|
||||
->select('favourites.*')
|
||||
|
||||
@@ -25,13 +25,9 @@ class CustomListItemRenderer implements NodeRendererInterface
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
|
||||
{
|
||||
if (!($node instanceof ListItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$listItem = $this->baseRenderer->render($node, $childRenderer);
|
||||
|
||||
if ($this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
|
||||
if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
|
||||
$listItem->setAttribute('class', 'task-list-item');
|
||||
}
|
||||
|
||||
|
||||
@@ -421,41 +421,28 @@ class PageContent
|
||||
*/
|
||||
protected function headerNodesToLevelList(DOMNodeList $nodeList): array
|
||||
{
|
||||
$minLevel = 6;
|
||||
|
||||
$headerDetails = array_map(function (DOMNode $header) use (&$minLevel) {
|
||||
if (!$header instanceof DOMElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tree = collect($nodeList)->map(function (DOMElement $header) {
|
||||
$text = trim(str_replace("\xc2\xa0", ' ', $header->nodeValue));
|
||||
$text = mb_substr($text, 0, 100);
|
||||
|
||||
if (empty($text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$level = intval(str_replace('h', '', $header->nodeName));
|
||||
if ($level < $minLevel) {
|
||||
$minLevel = $level;
|
||||
}
|
||||
|
||||
return [
|
||||
'nodeName' => strtolower($header->nodeName),
|
||||
'level' => $level,
|
||||
'level' => intval(str_replace('h', '', $header->nodeName)),
|
||||
'link' => '#' . $header->getAttribute('id'),
|
||||
'text' => $text,
|
||||
];
|
||||
}, [...$nodeList]);
|
||||
|
||||
$filtered = array_values(array_filter($headerDetails));
|
||||
})->filter(function ($header) {
|
||||
return mb_strlen($header['text']) > 0;
|
||||
});
|
||||
|
||||
// Shift headers if only smaller headers have been used
|
||||
$levelChange = ($minLevel - 1);
|
||||
foreach ($filtered as $index => $header) {
|
||||
$filtered[$index]['level'] -= $levelChange;
|
||||
}
|
||||
$levelChange = ($tree->pluck('level')->min() - 1);
|
||||
$tree = $tree->map(function ($header) use ($levelChange) {
|
||||
$header['level'] -= ($levelChange);
|
||||
|
||||
return $filtered;
|
||||
return $header;
|
||||
});
|
||||
|
||||
return $tree->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +192,10 @@ class PageIncludeParser
|
||||
/**
|
||||
* Get the parent paragraph of the given node, if existing.
|
||||
*/
|
||||
protected function getParentParagraph(DOMNode $parent): ?DOMElement
|
||||
protected function getParentParagraph(DOMNode $parent): ?DOMNode
|
||||
{
|
||||
do {
|
||||
if (strtolower($parent->nodeName) === 'p' && $parent instanceof DOMElement) {
|
||||
if (strtolower($parent->nodeName) === 'p') {
|
||||
return $parent;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class SlugGenerator
|
||||
{
|
||||
$slug = Str::slug($name);
|
||||
if ($slug === '') {
|
||||
$slug = substr(md5(strval(rand(1, 500))), 0, 5);
|
||||
$slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
}
|
||||
|
||||
return $slug;
|
||||
|
||||
@@ -25,6 +25,7 @@ class Handler extends ExceptionHandler
|
||||
protected $dontReport = [
|
||||
NotFoundException::class,
|
||||
StoppedAuthenticationException::class,
|
||||
NotifyException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,18 +6,22 @@ use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
/**
|
||||
* An exception that is thrown to notify the user of something which went wrong.
|
||||
* Typically these should be translated messages since they will be shown to the end user
|
||||
* via a pop up notification error message in the UI.
|
||||
*
|
||||
* This exception is not intended to be used for internal system/application errors,
|
||||
* and therefore will not be logged by the exception handler.
|
||||
*/
|
||||
class NotifyException extends Exception implements Responsable, HttpExceptionInterface
|
||||
{
|
||||
public $message;
|
||||
public string $redirectLocation;
|
||||
protected int $status;
|
||||
|
||||
public function __construct(string $message, string $redirectLocation = '/', int $status = 500)
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
public string $redirectLocation = '/',
|
||||
protected int $status = 500
|
||||
) {
|
||||
$this->message = $message;
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
$this->status = $status;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
namespace BookStack\Exports;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
@@ -121,9 +119,6 @@ class ImportRepo
|
||||
$parentModel = null;
|
||||
if ($import->type === 'page' || $import->type === 'chapter') {
|
||||
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null;
|
||||
if ($parentModel && !($parentModel instanceof Book || $parentModel instanceof Chapter)) {
|
||||
throw new ZipImportException(['Selected parent is not a book or chapter']);
|
||||
}
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
@@ -48,7 +48,7 @@ class ZipImportRunner
|
||||
* Returns the top-level entity item which was imported.
|
||||
* @throws ZipImportException
|
||||
*/
|
||||
public function run(Import $import, Book|Chapter|null $parent = null): Entity
|
||||
public function run(Import $import, ?Entity $parent = null): Entity
|
||||
{
|
||||
$zipPath = $this->getZipPath($import);
|
||||
$reader = new ZipExportReader($zipPath);
|
||||
|
||||
@@ -7,7 +7,6 @@ use BookStack\App\Model;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\Users\Models\OwnableInterface;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -81,7 +80,7 @@ abstract class Controller extends BaseController
|
||||
/**
|
||||
* Check the current user's permissions against an ownable item otherwise throw an exception.
|
||||
*/
|
||||
protected function checkOwnablePermission(string|Permission $permission, Model&OwnableInterface $ownable, string $redirectLocation = '/'): void
|
||||
protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void
|
||||
{
|
||||
if (!userCan($permission, $ownable)) {
|
||||
$this->showPermissionError($redirectLocation);
|
||||
|
||||
@@ -101,7 +101,6 @@ class JointPermissionBuilder
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with its children.
|
||||
* @return Builder<Book>
|
||||
*/
|
||||
protected function bookFetchQuery(): Builder
|
||||
{
|
||||
@@ -118,11 +117,9 @@ class JointPermissionBuilder
|
||||
|
||||
/**
|
||||
* Build joint permissions for the given book and role combinations.
|
||||
* @param EloquentCollection<int, Book> $books
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void
|
||||
{
|
||||
/** @var EloquentCollection<int, Entity> $entities */
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace BookStack\References;
|
||||
|
||||
use BookStack\App\Model;
|
||||
use BookStack\Permissions\Models\JointPermission;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ class ReferenceFetcher
|
||||
*/
|
||||
public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection
|
||||
{
|
||||
/** @var Collection<int, Reference> $references */
|
||||
$references = $this->queryReferencesToEntity($entity)->get();
|
||||
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents);
|
||||
|
||||
@@ -38,9 +37,6 @@ class ReferenceFetcher
|
||||
return $this->queryReferencesToEntity($entity)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<Reference>
|
||||
*/
|
||||
protected function queryReferencesToEntity(Entity $entity): Builder
|
||||
{
|
||||
$baseQuery = Reference::query()
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace BookStack\Search;
|
||||
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\EntityTable;
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
use BookStack\Entities\Tools\EntityHydrator;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
@@ -91,8 +90,6 @@ class SearchRunner
|
||||
|
||||
/**
|
||||
* Get a page of result data from the given query based on the provided page parameters.
|
||||
* @param EloquentBuilder<EntityTable> $query
|
||||
* @return Collection<Entity>
|
||||
*/
|
||||
protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection
|
||||
{
|
||||
@@ -109,7 +106,6 @@ class SearchRunner
|
||||
/**
|
||||
* Create a search query for an entity.
|
||||
* @param string[] $entityTypes
|
||||
* @return EloquentBuilder<EntityTable>
|
||||
*/
|
||||
protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder
|
||||
{
|
||||
@@ -294,7 +290,7 @@ class SearchRunner
|
||||
$query->where('name', '=', $tagParts['name']);
|
||||
}
|
||||
|
||||
if (is_numeric($tagParts['value']) && $tagParts['operator'] !== 'like') {
|
||||
if (is_numeric($tagParts['value']) && is_finite($tagParts['value']) && $tagParts['operator'] !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
|
||||
@@ -243,9 +243,9 @@ class SettingService
|
||||
/**
|
||||
* Convert a setting key into a user-specific key.
|
||||
*/
|
||||
protected function userKey(int $userId, string $key = ''): string
|
||||
protected function userKey(string $userId, string $key = ''): string
|
||||
{
|
||||
return 'user:' . strval($userId) . ':' . $key;
|
||||
return 'user:' . $userId . ':' . $key;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -267,7 +267,7 @@ class SettingService
|
||||
/**
|
||||
* Delete settings for a given user id.
|
||||
*/
|
||||
public function deleteUserSettings(int $userId): void
|
||||
public function deleteUserSettings(string $userId): void
|
||||
{
|
||||
Setting::query()
|
||||
->where('setting_key', 'like', $this->userKey($userId) . '%')
|
||||
|
||||
@@ -4,37 +4,12 @@ namespace BookStack\Sorting;
|
||||
|
||||
class BookSortMapItem
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $sort;
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
public $parentChapterId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $parentBookId;
|
||||
|
||||
public function __construct(int $id, int $sort, ?int $parentChapterId, string $type, int $parentBookId)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->sort = $sort;
|
||||
$this->parentChapterId = $parentChapterId;
|
||||
$this->type = $type;
|
||||
$this->parentBookId = $parentBookId;
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public int $sort,
|
||||
public int|null $parentChapterId,
|
||||
public string $type,
|
||||
public int $parentBookId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ class BookSorter
|
||||
$model->priority = $sortMapItem->sort;
|
||||
}
|
||||
|
||||
if ($chapterChanged || $priorityChanged) {
|
||||
if ($priorityChanged || $chapterChanged) {
|
||||
$model::withoutTimestamps(fn () => $model->save());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,35 +38,21 @@ class SortRule extends Model implements Loggable
|
||||
$this->sequence = implode(',', $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
return "({$this->id}) {$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to where this rule can be managed.
|
||||
*/
|
||||
public function getUrl(): string
|
||||
{
|
||||
return url("/settings/sorting/rules/{$this->id}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the books which are specifically set to use this sort rule.
|
||||
* @return HasMany<Book, $this>
|
||||
*/
|
||||
public function books(): HasMany
|
||||
{
|
||||
return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the available sort rules, ordered by name, with the number of books using each.
|
||||
* @return Collection<SortRule>
|
||||
*/
|
||||
public static function allByName(): Collection
|
||||
{
|
||||
return static::query()
|
||||
|
||||
@@ -10,6 +10,7 @@ use BookStack\Permissions\PermissionApplicator;
|
||||
use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||
use BookStack\Users\Models\OwnableInterface;
|
||||
use BookStack\Users\Models\User;
|
||||
use BookStack\Util\UrlFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -71,7 +72,7 @@ class Attachment extends Model implements OwnableInterface
|
||||
public function getUrl($openInline = false): string
|
||||
{
|
||||
if ($this->external && !str_starts_with($this->path, 'http')) {
|
||||
return $this->path;
|
||||
return (new UrlFilter($this->path))->clean();
|
||||
}
|
||||
|
||||
return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));
|
||||
|
||||
@@ -102,7 +102,7 @@ class AttachmentService
|
||||
/**
|
||||
* Updates the ordering for a listing of attached files.
|
||||
*/
|
||||
public function updateFileOrderWithinPage(array $attachmentOrder, int $pageId)
|
||||
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
|
||||
{
|
||||
foreach ($attachmentOrder as $index => $attachmentId) {
|
||||
Attachment::query()->where('uploaded_to', '=', $pageId)
|
||||
|
||||
@@ -11,6 +11,7 @@ use BookStack\Http\Controller;
|
||||
use BookStack\Permissions\Permission;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use BookStack\Util\UrlFilter;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -107,6 +108,9 @@ class AttachmentController extends Controller
|
||||
{
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission(Permission::PageView, $attachment->page);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);
|
||||
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
|
||||
|
||||
try {
|
||||
$this->validate($request, [
|
||||
@@ -120,10 +124,6 @@ class AttachmentController extends Controller
|
||||
]), 422);
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission(Permission::PageView, $attachment->page);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);
|
||||
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
|
||||
|
||||
$attachment = $this->attachmentService->updateFile($attachment, [
|
||||
'name' => $request->input('attachment_edit_name'),
|
||||
'link' => $request->input('attachment_edit_url'),
|
||||
@@ -142,6 +142,10 @@ class AttachmentController extends Controller
|
||||
public function attachLink(Request $request)
|
||||
{
|
||||
$pageId = $request->input('attachment_link_uploaded_to');
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
|
||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
try {
|
||||
$this->validate($request, [
|
||||
@@ -156,11 +160,6 @@ class AttachmentController extends Controller
|
||||
]), 422);
|
||||
}
|
||||
|
||||
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
|
||||
|
||||
$this->checkPermission(Permission::AttachmentCreateAll);
|
||||
$this->checkOwnablePermission(Permission::PageUpdate, $page);
|
||||
|
||||
$attachmentName = $request->input('attachment_link_name');
|
||||
$link = $request->input('attachment_link_url');
|
||||
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
@@ -223,7 +222,8 @@ class AttachmentController extends Controller
|
||||
}
|
||||
|
||||
if ($attachment->external) {
|
||||
return redirect($attachment->path);
|
||||
$url = (new UrlFilter($attachment->path))->clean();
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
$fileName = $attachment->getFileName();
|
||||
|
||||
@@ -87,7 +87,7 @@ class RoleApiController extends ApiController
|
||||
*/
|
||||
public function read(string $id)
|
||||
{
|
||||
$role = $this->permissionsRepo->getRoleById(intval($id));
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$this->singleFormatter($role);
|
||||
|
||||
return response()->json($role);
|
||||
|
||||
@@ -94,7 +94,7 @@ class RoleController extends Controller
|
||||
public function edit(string $id)
|
||||
{
|
||||
$this->checkPermission(Permission::UserRolesManage);
|
||||
$role = $this->permissionsRepo->getRoleById(intval($id));
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
|
||||
$this->setPageTitle(trans('settings.role_edit'));
|
||||
|
||||
@@ -129,7 +129,7 @@ class RoleController extends Controller
|
||||
public function showDelete(string $id)
|
||||
{
|
||||
$this->checkPermission(Permission::UserRolesManage);
|
||||
$role = $this->permissionsRepo->getRoleById(intval($id));
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||
$roles->prepend($blankRole);
|
||||
@@ -151,7 +151,7 @@ class RoleController extends Controller
|
||||
|
||||
try {
|
||||
$migrateRoleId = intval($request->input('migrate_role_id') ?: "0");
|
||||
$this->permissionsRepo->deleteRole(intval($id), $migrateRoleId);
|
||||
$this->permissionsRepo->deleteRole($id, $migrateRoleId);
|
||||
} catch (PermissionsException $e) {
|
||||
$this->showErrorNotification($e->getMessage());
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class UserApiController extends ApiController
|
||||
*/
|
||||
public function read(string $id)
|
||||
{
|
||||
$user = $this->userRepo->getById(intval($id));
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->singleFormatter($user);
|
||||
|
||||
return response()->json($user);
|
||||
@@ -124,8 +124,8 @@ class UserApiController extends ApiController
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$data = $this->validate($request, $this->rules(intval($id))['update']);
|
||||
$user = $this->userRepo->getById(intval($id));
|
||||
$data = $this->validate($request, $this->rules($id)['update']);
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->userRepo->update($user, $data, userCan(Permission::UsersManage));
|
||||
$this->singleFormatter($user);
|
||||
|
||||
@@ -140,7 +140,7 @@ class UserApiController extends ApiController
|
||||
*/
|
||||
public function delete(Request $request, string $id)
|
||||
{
|
||||
$user = $this->userRepo->getById(intval($id));
|
||||
$user = $this->userRepo->getById($id);
|
||||
$newOwnerId = $request->input('migrate_ownership_id', null);
|
||||
|
||||
$this->userRepo->destroy($user, $newOwnerId);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Util;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
@@ -25,7 +24,7 @@ use Throwable;
|
||||
class DatabaseTransaction
|
||||
{
|
||||
/**
|
||||
* @param (Closure(Connection): TReturn) $callback
|
||||
* @param (Closure(static): TReturn) $callback
|
||||
*/
|
||||
public function __construct(
|
||||
protected Closure $callback
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Util;
|
||||
|
||||
use BookStack\Util\HtmlPurifier\ConfiguredHtmlPurifier;
|
||||
use DOMAttr;
|
||||
use DOMElement;
|
||||
use DOMNodeList;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Util;
|
||||
namespace BookStack\Util\HtmlPurifier;
|
||||
|
||||
use BookStack\App\AppVersion;
|
||||
use BookStack\Util\HtmlPurifier\Filters\UriLimitFileProtocolToAnchors;
|
||||
use BookStack\Util\UrlFilter;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
use HTMLPurifier_DefinitionCache_Serializer;
|
||||
use HTMLPurifier_HTML5Config;
|
||||
use HTMLPurifier_HTMLDefinition;
|
||||
use HTMLPurifier_URIDefinition;
|
||||
|
||||
/**
|
||||
* Provides a configured HTML Purifier instance.
|
||||
@@ -33,7 +36,12 @@ class ConfiguredHtmlPurifier
|
||||
|
||||
$htmlDef = $config->getDefinition('HTML', true, true);
|
||||
if ($htmlDef instanceof HTMLPurifier_HTMLDefinition) {
|
||||
$this->configureDefinition($htmlDef);
|
||||
$this->configureHtmlDefinition($htmlDef);
|
||||
}
|
||||
|
||||
$uriDef = $config->getDefinition('URI', true, true);
|
||||
if ($uriDef instanceof HTMLPurifier_URIDefinition) {
|
||||
$this->configureUriDefinition($uriDef);
|
||||
}
|
||||
|
||||
$this->purifier = new HTMLPurifier($config);
|
||||
@@ -77,21 +85,18 @@ class ConfiguredHtmlPurifier
|
||||
$config->set('Attr.ID.HTML5', true);
|
||||
$config->set('Output.FixInnerHTML', false);
|
||||
$config->set('URI.SafeIframeRegexp', '%^(http://|https://|//)%');
|
||||
$config->set('URI.AllowedSchemes', [
|
||||
'http' => true,
|
||||
'https' => true,
|
||||
'mailto' => true,
|
||||
'ftp' => true,
|
||||
'nntp' => true,
|
||||
'news' => true,
|
||||
'tel' => true,
|
||||
'file' => true,
|
||||
]);
|
||||
|
||||
$allowedSchemes = UrlFilter::getAllowedSchemes();
|
||||
$allowedSchemesSetting = [];
|
||||
foreach ($allowedSchemes as $scheme) {
|
||||
$allowedSchemesSetting[$scheme] = true;
|
||||
}
|
||||
$config->set('URI.AllowedSchemes', $allowedSchemesSetting);
|
||||
|
||||
// $config->set('Cache.DefinitionImpl', null); // Disable cache during testing
|
||||
}
|
||||
|
||||
public function configureDefinition(HTMLPurifier_HTMLDefinition $definition): void
|
||||
protected function configureHtmlDefinition(HTMLPurifier_HTMLDefinition $definition): void
|
||||
{
|
||||
// Allow the object element
|
||||
$definition->addElement(
|
||||
@@ -149,6 +154,15 @@ class ConfiguredHtmlPurifier
|
||||
|
||||
// Allow mention-ids on links
|
||||
$definition->addAttribute('a', 'data-mention-user-id', 'Number');
|
||||
|
||||
// Set up custom handler for srcset to limit accepted types
|
||||
$definition->addAttribute('img', 'srcset', new SrcsetAttrDef());
|
||||
$definition->addAttribute('source', 'srcset', new SrcsetAttrDef());
|
||||
}
|
||||
|
||||
protected function configureUriDefinition(HTMLPurifier_URIDefinition $definition): void
|
||||
{
|
||||
$definition->registerFilter(new UriLimitFileProtocolToAnchors());
|
||||
}
|
||||
|
||||
public function purify(string $html): string
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Util\HtmlPurifier\Filters;
|
||||
|
||||
use HTMLPurifier_Config;
|
||||
use HTMLPurifier_Context;
|
||||
use HTMLPurifier_URI;
|
||||
use HTMLPurifier_URIFilter;
|
||||
|
||||
/**
|
||||
* Limits file:// URIs to only be used on anchor tags href attributes.
|
||||
* This prevents use on iframes/embeds/images where they can be used to load external
|
||||
* content on the network, triggering calls which may include NTLM auth hashes when in
|
||||
* certain windows based environments.
|
||||
*/
|
||||
class UriLimitFileProtocolToAnchors extends HTMLPurifier_URIFilter
|
||||
{
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
public $name = 'LimitFileProtocolToAnchors';
|
||||
|
||||
/**
|
||||
* @type bool
|
||||
*/
|
||||
public $always_load = true;
|
||||
|
||||
/**
|
||||
* @param HTMLPurifier_URI $uri
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return bool
|
||||
*/
|
||||
public function filter(&$uri, $config, $context)
|
||||
{
|
||||
// Ensure we're only filtering file:// URIs'
|
||||
if ($uri->scheme !== 'file') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$token = $context->get('CurrentToken', true);
|
||||
$attr = $context->get('CurrentAttr', true);
|
||||
|
||||
// Only allow if used on hrefs on anchor tags
|
||||
$isAnchor = $token && $token->name === 'a';
|
||||
$isHref = $attr === 'href';
|
||||
if ($isAnchor && $isHref) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
26
app/Util/HtmlPurifier/SrcsetAttrDef.php
Normal file
26
app/Util/HtmlPurifier/SrcsetAttrDef.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Util\HtmlPurifier;
|
||||
|
||||
use HTMLPurifier_AttrDef;
|
||||
|
||||
/**
|
||||
* Custom attribute definition to filter out potentially dangerous
|
||||
* values from the srcset attribute.
|
||||
*/
|
||||
class SrcsetAttrDef extends HTMLPurifier_AttrDef
|
||||
{
|
||||
public function validate($string, $config, $context)
|
||||
{
|
||||
$lower = strtolower($string);
|
||||
$nonAllowed = ['javascript:', 'vbscript:', 'data:', 'file:'];
|
||||
|
||||
foreach ($nonAllowed as $nonAllowedString) {
|
||||
if (str_contains($lower, $nonAllowedString)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
36
app/Util/UrlComparison.php
Normal file
36
app/Util/UrlComparison.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Util;
|
||||
|
||||
class UrlComparison
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly string $a,
|
||||
protected readonly string $b,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the two URLs have the same origin.
|
||||
*/
|
||||
public function originsMatch(): bool
|
||||
{
|
||||
$aParts = parse_url($this->a);
|
||||
$bParts = parse_url($this->b);
|
||||
|
||||
return ($aParts['host'] ?? '') === ($bParts['host'] ?? '')
|
||||
&& ($aParts['scheme'] ?? '') === ($bParts['scheme'] ?? '')
|
||||
&& ($aParts['port'] ?? '') === ($bParts['port'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's some overlap between the two URLs' paths.
|
||||
*/
|
||||
public function pathsOverlap(): bool
|
||||
{
|
||||
$aPath = parse_url($this->a, PHP_URL_PATH) ?? '';
|
||||
$bPath = parse_url($this->b, PHP_URL_PATH) ?? '';
|
||||
|
||||
return str_starts_with($aPath, $bPath) || str_starts_with($bPath, $aPath);
|
||||
}
|
||||
}
|
||||
103
app/Util/UrlFilter.php
Normal file
103
app/Util/UrlFilter.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BookStack\Util;
|
||||
|
||||
/**
|
||||
* Helps filter URLs to prevent use of undesired schemes.
|
||||
* Also parses and rebuilds the URL to ensure it's valid.
|
||||
*/
|
||||
class UrlFilter
|
||||
{
|
||||
protected static array $allowedSchemes = ['http', 'https', 'mailto', 'tel', 'file', 'ftp', 'nntp', 'news'];
|
||||
protected string $url;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$this->url = trim($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the URL is allowed to be generally used as a link
|
||||
* in the application. This does not assure the original URL string
|
||||
* provided is safe as-is. Ensure you use the clean method to produce
|
||||
* a URL that is considered safe to use.
|
||||
*/
|
||||
public function isAllowed(): bool
|
||||
{
|
||||
$urlParts = parse_url($this->url);
|
||||
if (!$urlParts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extra check to help avoid scenarios where non-standard characters are used in the scheme
|
||||
// to work around parse_url handling with URLs which may be interpreted by the browser differently.
|
||||
if (str_contains($this->url, ':') && !preg_match('/^[a-z]+:/i', $this->url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($urlParts['scheme'])) {
|
||||
return in_array(strtolower($urlParts['scheme']), self::$allowedSchemes);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the URL to ensure it's valid and only uses the allowed schemes.
|
||||
* If the URL is not allowed, return a placeholder.
|
||||
*/
|
||||
public function clean(): string
|
||||
{
|
||||
if (!$this->isAllowed()) {
|
||||
return '#badlink';
|
||||
}
|
||||
|
||||
$urlParts = parse_url($this->url);
|
||||
if (!$urlParts) {
|
||||
return '#badlink';
|
||||
}
|
||||
|
||||
$url = '';
|
||||
|
||||
if (isset($urlParts['scheme']) || isset($urlParts['host'])) {
|
||||
$scheme = strtolower($urlParts['scheme'] ?? 'https');
|
||||
$url = $scheme . ':' . (isset($urlParts['host']) ? '//' : '');
|
||||
}
|
||||
|
||||
if (isset($urlParts['user']) || isset($urlParts['pass'])) {
|
||||
$url .= $urlParts['user'] ?? '';
|
||||
if (isset($urlParts['pass'])) {
|
||||
$url .= ':' . $urlParts['pass'];
|
||||
}
|
||||
$url .= '@';
|
||||
}
|
||||
|
||||
if (isset($urlParts['host'])) {
|
||||
$url .= $urlParts['host'];
|
||||
}
|
||||
if (isset($urlParts['port'])) {
|
||||
$url .= ':' . $urlParts['port'];
|
||||
}
|
||||
if (isset($urlParts['path'])) {
|
||||
$url .= $urlParts['path'];
|
||||
}
|
||||
if (isset($urlParts['query'])) {
|
||||
$url .= '?' . $urlParts['query'];
|
||||
}
|
||||
if (isset($urlParts['fragment'])) {
|
||||
$url .= '#' . $urlParts['fragment'];
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schemes that are allowed to be used in content links.
|
||||
*/
|
||||
public static function getAllowedSchemes(): array
|
||||
{
|
||||
return self::$allowedSchemes;
|
||||
}
|
||||
}
|
||||
458
composer.lock
generated
458
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -482,8 +482,8 @@ electron-to-chromium
|
||||
License: ISC
|
||||
License File: node_modules/electron-to-chromium/LICENSE
|
||||
Copyright: Copyright 2018 Kilian Valkhof
|
||||
Source: git+https://github.com/kilian/electron-to-chromium.git
|
||||
Link: git+https://github.com/kilian/electron-to-chromium.git
|
||||
Source: git+https://github.com/Kilian/electron-to-chromium.git
|
||||
Link: git+https://github.com/Kilian/electron-to-chromium.git
|
||||
-----------
|
||||
emittery
|
||||
License: MIT
|
||||
@@ -860,7 +860,7 @@ License: MIT
|
||||
License File: node_modules/handlebars/LICENSE
|
||||
Copyright: Copyright (C) 2011-2019 by Yehuda Katz
|
||||
Source: https://github.com/handlebars-lang/handlebars.js.git
|
||||
Link: https://www.handlebarsjs.com/
|
||||
Link: https://handlebarsjs.com/
|
||||
-----------
|
||||
has-bigints
|
||||
License: MIT
|
||||
@@ -1663,13 +1663,6 @@ Copyright: Copyright (c) Stephen Sugden <**@*************.***> (stephensugden.co
|
||||
Source: grncdr/merge-stream
|
||||
Link: grncdr/merge-stream
|
||||
-----------
|
||||
micromatch
|
||||
License: MIT
|
||||
License File: node_modules/micromatch/LICENSE
|
||||
Copyright: Copyright (c) 2014-present, Jon Schlinkert.
|
||||
Source: micromatch/micromatch
|
||||
Link: https://github.com/micromatch/micromatch
|
||||
-----------
|
||||
mimic-fn
|
||||
License: MIT
|
||||
License File: node_modules/mimic-fn/license
|
||||
@@ -3574,13 +3567,6 @@ Copyright: Copyright (c) 2017-present Devon Govett
|
||||
Source: https://github.com/parcel-bundler/watcher.git
|
||||
Link: https://github.com/parcel-bundler/watcher.git
|
||||
-----------
|
||||
@parcel/watcher-linux-x64-musl
|
||||
License: MIT
|
||||
License File: node_modules/@parcel/watcher-linux-x64-musl/LICENSE
|
||||
Copyright: Copyright (c) 2017-present Devon Govett
|
||||
Source: https://github.com/parcel-bundler/watcher.git
|
||||
Link: https://github.com/parcel-bundler/watcher.git
|
||||
-----------
|
||||
@parcel/watcher
|
||||
License: MIT
|
||||
License File: node_modules/@parcel/watcher/LICENSE
|
||||
@@ -3603,8 +3589,8 @@ Link: https://github.com/un-ts/pkgr/blob/master/packages/core
|
||||
License: MIT
|
||||
License File: node_modules/@sinclair/typebox/license
|
||||
Copyright: Copyright (c) 2017-2026 Haydn Paterson
|
||||
Source: https://github.com/sinclairzx81/typebox-legacy
|
||||
Link: https://github.com/sinclairzx81/typebox-legacy
|
||||
Source: https://github.com/sinclairzx81/sinclair-typebox
|
||||
Link: https://github.com/sinclairzx81/sinclair-typebox
|
||||
-----------
|
||||
@sinonjs/commons
|
||||
License: BSD-3-Clause
|
||||
@@ -3817,8 +3803,3 @@ Link: https://github.com/ungap/structured-clone#readme
|
||||
License: MIT
|
||||
Source: git+https://github.com/unrs/unrs-resolver.git
|
||||
Link: https://github.com/unrs/unrs-resolver#readme
|
||||
-----------
|
||||
@unrs/resolver-binding-linux-x64-musl
|
||||
License: MIT
|
||||
Source: git+https://github.com/unrs/unrs-resolver.git
|
||||
Link: https://github.com/unrs/unrs-resolver#readme
|
||||
|
||||
@@ -110,7 +110,7 @@ License: BSD-3-Clause
|
||||
License File: vendor/firebase/php-jwt/LICENSE
|
||||
Copyright: Copyright (c) 2011, Neuman Vong
|
||||
Source: https://github.com/googleapis/php-jwt.git
|
||||
Link: https://github.com/firebase/php-jwt
|
||||
Link: https://github.com/googleapis/php-jwt
|
||||
-----------
|
||||
fruitcake/php-cors
|
||||
License: MIT
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -99,8 +99,8 @@ return [
|
||||
'user_update_notification' => 'Uživatel byl úspěšně aktualizován',
|
||||
'user_delete' => 'odstranil uživatele',
|
||||
'user_delete_notification' => 'Uživatel byl úspěšně odstraněn',
|
||||
'user_mfa_reset' => 'reset MFA for user',
|
||||
'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
|
||||
'user_mfa_reset' => 'obnovit vícefaktorové ověření pro uživatele',
|
||||
'user_mfa_reset_notification' => 'Obnovení metod vícefaktorového ověřování',
|
||||
|
||||
// API Tokens
|
||||
'api_token_create' => 'API token byl vytvořen',
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|
||||
'failed' => 'Neplatné přihlašovací údaje.',
|
||||
'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.',
|
||||
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
|
||||
'mfa_throttle' => '{0}Příliš mnoho pokusů o vícefázové ověření. Zkuste to prosím znovu za :seconds sekund.|{1}Příliš mnoho pokusů o vícefázové ověření. Zkuste to prosím znovu za :seconds sekundu.|[2,4]Příliš mnoho pokusů o vícefázové ověření. Zkuste to prosím znovu za :seconds sekundy.|[5,*]Příliš mnoho pokusů o vícefázové ověření. Zkuste to prosím znovu za :seconds sekund.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Registrace',
|
||||
|
||||
@@ -173,7 +173,7 @@ return [
|
||||
'books_sort_desc' => 'Pro přeuspořádání obsahu přesuňte kapitoly a stránky v knize. Mohou být přidány další knihy, které umožní snadný přesun kapitol a stránek mezi knihami. Volitelně lze nastavit pravidlo automatického řazení, aby se při změnách automaticky seřadil obsah této knihy.',
|
||||
'books_sort_auto_sort' => 'Možnost automatického řazení',
|
||||
'books_sort_auto_sort_active' => 'Aktivní automatické řazení: :sortName',
|
||||
'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.',
|
||||
'books_sort_auto_sort_creation_hint' => 'Pravidla řazení mohou být vytvořena v nastavení "Seznamy a řazení" uživatelem s příslušnými oprávněními.',
|
||||
'books_sort_named' => 'Seřadit knihu :bookName',
|
||||
'books_sort_name' => 'Seřadit podle názvu',
|
||||
'books_sort_created' => 'Seřadit podle data vytvoření',
|
||||
@@ -331,9 +331,9 @@ return [
|
||||
|
||||
// Editor Sidebar
|
||||
'toggle_sidebar' => 'Skrýt/Zobrazit postranní panel',
|
||||
'page_contents' => 'Page Contents',
|
||||
'page_contents_none' => 'No headings were found in the page content.',
|
||||
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
|
||||
'page_contents' => 'Obsah stránky',
|
||||
'page_contents_none' => 'Na stránce nejsou žádné nadpisy.',
|
||||
'page_contents_info' => 'Obsah se generuje ze všech použitých nadpisů na stránce.',
|
||||
'page_tags' => 'Štítky stránky',
|
||||
'chapter_tags' => 'Štítky kapitoly',
|
||||
'book_tags' => 'Štítky knihy',
|
||||
|
||||
@@ -207,7 +207,7 @@ return [
|
||||
'role_all' => 'Vše',
|
||||
'role_own' => 'Vlastní',
|
||||
'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',
|
||||
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
|
||||
'role_controlled_by_page_delete' => 'Řídí se právem k odstranění stránky',
|
||||
'role_save' => 'Uložit roli',
|
||||
'role_users' => 'Uživatelé mající tuto roli',
|
||||
'role_users_none' => 'Žádný uživatel nemá tuto roli',
|
||||
@@ -260,13 +260,13 @@ return [
|
||||
'users_api_tokens_create' => 'Vytvořit Token',
|
||||
'users_api_tokens_expires' => 'Vyprší',
|
||||
'users_api_tokens_docs' => 'Dokumentace API',
|
||||
'users_mfa' => 'Vícefázové ověření',
|
||||
'users_mfa' => 'Vícefaktorové ověření',
|
||||
'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',
|
||||
'users_mfa_x_methods' => ':count nastavená metoda|:count nastavených metod',
|
||||
'users_mfa_configure' => 'Konfigurovat metody',
|
||||
'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods',
|
||||
'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.',
|
||||
'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?',
|
||||
'users_mfa_reset' => 'Obnova metod vícefaktorového ověření',
|
||||
'users_mfa_reset_desc' => 'Tímto se obnoví a vymažou všechny nakonfigurované metody vícefaktorového ověřování pro tohoto uživatele. Pokud některá z jeho rolí vyžaduje vícefaktorové ověřování, bude při příštím přihlášení vyzván k nastavení nových metod.',
|
||||
'users_mfa_reset_confirm' => 'Opravdu chcete obnovit vícefaktorové ověřování pro tohoto uživatele?',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Vytvořit API Token',
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|
||||
'failed' => 'De indtastede brugeroplysninger stemmer ikke overens med vores registreringer.',
|
||||
'throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.',
|
||||
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
|
||||
'mfa_throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Registrer',
|
||||
|
||||
@@ -331,9 +331,9 @@ return [
|
||||
|
||||
// Editor Sidebar
|
||||
'toggle_sidebar' => 'Sidebjælke til/fra',
|
||||
'page_contents' => 'Page Contents',
|
||||
'page_contents_none' => 'No headings were found in the page content.',
|
||||
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
|
||||
'page_contents' => 'Sideindhold',
|
||||
'page_contents_none' => 'Ingen overskrifter blev fundet i sidens indhold.',
|
||||
'page_contents_info' => 'Indholdsmenuen er genereret fra alle kursformater, der bruges på siden.',
|
||||
'page_tags' => 'Sidetags',
|
||||
'chapter_tags' => 'Kapiteltags',
|
||||
'book_tags' => 'Bogtags',
|
||||
|
||||
@@ -366,8 +366,9 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'th' => 'Thailandsk',
|
||||
'tr' => 'Türkçe',
|
||||
'uk' => 'Українська',
|
||||
'uz' => 'O‘zbekcha',
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|
||||
'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.',
|
||||
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
|
||||
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
|
||||
'mfa_throttle' => 'Zu viele Multi-Faktor-Verifizierungsversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Registrieren',
|
||||
|
||||
@@ -173,7 +173,7 @@ return [
|
||||
'books_sort_desc' => 'Verschieben Sie Kapitel und Seiten innerhalb eines Buches, um dessen Inhalt neu zu ordnen. Es können weitere Bücher hinzugefügt werden, wodurch Kapitel und Seiten problemlos zwischen den Büchern verschoben werden können. Optional kann eine automatische Sortierregel festgelegt werden, um den Inhalt dieses Buches bei Änderungen automatisch zu sortieren.',
|
||||
'books_sort_auto_sort' => 'Automatische Sortierfunktionsoption',
|
||||
'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName',
|
||||
'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.',
|
||||
'books_sort_auto_sort_creation_hint' => 'Regeln für die automatische Sortierung können von einem Benutzer mit den entsprechenden Berechtigungen im Einstellungsbereich "Listen & Sortieren" erstellt werden.',
|
||||
'books_sort_named' => 'Buch ":bookName" sortieren',
|
||||
'books_sort_name' => 'Sortieren nach Namen',
|
||||
'books_sort_created' => 'Sortieren nach Erstellungsdatum',
|
||||
@@ -331,9 +331,9 @@ return [
|
||||
|
||||
// Editor Sidebar
|
||||
'toggle_sidebar' => 'Seitenleiste umschalten',
|
||||
'page_contents' => 'Page Contents',
|
||||
'page_contents_none' => 'No headings were found in the page content.',
|
||||
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
|
||||
'page_contents' => 'Seiteninhalt',
|
||||
'page_contents_none' => 'Es wurden keine Überschriften im Seiteninhalt gefunden.',
|
||||
'page_contents_info' => 'Das Inhaltsmenü wird aus allen auf der Seite verwendeten Überschriften generiert.',
|
||||
'page_tags' => 'Seiten-Schlagwörter',
|
||||
'chapter_tags' => 'Kapitel-Schlagwörter',
|
||||
'book_tags' => 'Buch-Schlagwörter',
|
||||
|
||||
@@ -207,7 +207,7 @@ return [
|
||||
'role_all' => 'Alle',
|
||||
'role_own' => 'Eigene',
|
||||
'role_controlled_by_asset' => 'Abhängig von dem Asset, in das sie hochgeladen werden',
|
||||
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
|
||||
'role_controlled_by_page_delete' => 'Kontrolliert durch die Berechtigung zum Löschen einer Seite',
|
||||
'role_save' => 'Rolle speichern',
|
||||
'role_users' => 'Dieser Rolle zugeordnete Benutzer',
|
||||
'role_users_none' => 'Derzeit sind diesem Rollentyp keine Benutzer zugewiesen',
|
||||
@@ -265,7 +265,7 @@ return [
|
||||
'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',
|
||||
'users_mfa_configure' => 'Methoden konfigurieren',
|
||||
'users_mfa_reset' => 'Setze Multifaktor-Authentifizierung zurück',
|
||||
'users_mfa_reset_desc' => 'Dies wird alle konfigurierten Multifaktor-Authentifizierungsmethoden für diesen Nutzer zurücksetzen. Falls Multifaktor-Authentifizierung für eine seiner Rollen erforderlich ist, werden sie aufgefordert, neue Methoden beim nächsten Login zu konfigurieren.',
|
||||
'users_mfa_reset_desc' => 'Dies wird alle konfigurierten Multifaktor-Authentifizierungsmethoden für diesen Nutzer zurücksetzen. Falls Multifaktor-Authentifizierung für eine seiner Rollen erforderlich ist, wird der Nutzer aufgefordert, neue Methoden beim nächsten Login zu konfigurieren.',
|
||||
'users_mfa_reset_confirm' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchten?',
|
||||
|
||||
// API Tokens
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Russisch',
|
||||
'sk' => 'Slowenisch',
|
||||
'sl' => 'Slowenisch',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Schwedisch',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkisch',
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|
||||
'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',
|
||||
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.',
|
||||
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
|
||||
'mfa_throttle' => 'Zu viele Multi-Faktor-Verifizierungsversuche. Bitte versuche es in :seconds Sekunden erneut.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Registrieren',
|
||||
|
||||
@@ -173,7 +173,7 @@ return [
|
||||
'books_sort_desc' => 'Kapitel und Seiten innerhalb eines Buches verschieben, um dessen Inhalt zu reorganisieren. Andere Bücher können hinzugefügt werden, was das Verschieben von Kapiteln und Seiten zwischen Büchern erleichtert. Optional kann eine automatische Sortierregel erstellt werden, um den Inhalt dieses Buches nach Änderungen automatisch zu sortieren.',
|
||||
'books_sort_auto_sort' => 'Auto-Sortieroption',
|
||||
'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName',
|
||||
'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.',
|
||||
'books_sort_auto_sort_creation_hint' => 'Regeln für die automatische Sortierung können von einem Benutzer mit den entsprechenden Berechtigungen im Einstellungsbereich "Listen & Sortieren" erstellt werden.',
|
||||
'books_sort_named' => 'Buch ":bookName" sortieren',
|
||||
'books_sort_name' => 'Sortieren nach Namen',
|
||||
'books_sort_created' => 'Sortieren nach Erstellungsdatum',
|
||||
@@ -331,9 +331,9 @@ return [
|
||||
|
||||
// Editor Sidebar
|
||||
'toggle_sidebar' => 'Seitenleiste umschalten',
|
||||
'page_contents' => 'Page Contents',
|
||||
'page_contents_none' => 'No headings were found in the page content.',
|
||||
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
|
||||
'page_contents' => 'Seiteninhalt',
|
||||
'page_contents_none' => 'Es wurden keine Überschriften im Seiteninhalt gefunden.',
|
||||
'page_contents_info' => 'Das Inhaltsmenü wird aus allen auf der Seite verwendeten Überschriften generiert.',
|
||||
'page_tags' => 'Seiten-Schlagwörter',
|
||||
'chapter_tags' => 'Kapitel-Schlagwörter',
|
||||
'book_tags' => 'Buch-Schlagwörter',
|
||||
|
||||
@@ -208,7 +208,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'role_all' => 'Alle',
|
||||
'role_own' => 'Eigene',
|
||||
'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',
|
||||
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
|
||||
'role_controlled_by_page_delete' => 'Kontrolliert durch die Berechtigung zum Löschen einer Seite',
|
||||
'role_save' => 'Rolle speichern',
|
||||
'role_users' => 'Dieser Rolle zugeordnete Benutzer',
|
||||
'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet',
|
||||
@@ -266,8 +266,8 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',
|
||||
'users_mfa_configure' => 'Methoden konfigurieren',
|
||||
'users_mfa_reset' => 'Setze Multifaktor-Authentifizierung zurück',
|
||||
'users_mfa_reset_desc' => 'Dies wird alle konfigurierten Multifaktor-Authentifizierungsmethoden für diesen Nutzer zurücksetzen. Falls Multifaktor-Authentifizierung für eine seiner Rollen erforderlich ist, werden sie aufgefordert, neue Methoden beim nächsten Login zu konfigurieren.',
|
||||
'users_mfa_reset_confirm' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchten?',
|
||||
'users_mfa_reset_desc' => 'Dies wird alle konfigurierten Multifaktor-Authentifizierungsmethoden für diesen Nutzer zurücksetzen. Falls Multifaktor-Authentifizierung für eine seiner Rollen erforderlich ist, wird der Nutzer aufgefordert, neue Methoden beim nächsten Login zu konfigurieren.',
|
||||
'users_mfa_reset_confirm' => 'Bist du sicher, dass du diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchtest?',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Neuen API-Token erstellen',
|
||||
@@ -367,6 +367,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'ru' => 'Russisch',
|
||||
'sk' => 'Slowenisch',
|
||||
'sl' => 'Slowenisch',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Schwedisch',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkisch',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Ruso',
|
||||
'sk' => 'Eslovaco',
|
||||
'sl' => 'Esloveno',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Sueco',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Turco',
|
||||
|
||||
@@ -367,6 +367,7 @@ return [
|
||||
'ru' => 'Ruso',
|
||||
'sk' => 'Eslovaco',
|
||||
'sl' => 'Esloveno',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Sueco',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Turco',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский (vene keel)',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Sloveenia',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Rootsi',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türgi',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -99,8 +99,8 @@ return [
|
||||
'user_update_notification' => 'Utilisateur mis à jour avec succès',
|
||||
'user_delete' => 'utilisateur supprimé',
|
||||
'user_delete_notification' => 'Utilisateur supprimé avec succès',
|
||||
'user_mfa_reset' => 'reset MFA for user',
|
||||
'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
|
||||
'user_mfa_reset' => 'réinitialiser l\'authentification multifacteur pour l\'utilisateur',
|
||||
'user_mfa_reset_notification' => 'Les méthodes d\'authentification multifacteurs sont réinitialisées',
|
||||
|
||||
// API Tokens
|
||||
'api_token_create' => 'a créé un jeton API',
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
|
||||
'failed' => 'Ces informations ne correspondent à aucun compte.',
|
||||
'throttle' => 'Trop d\'essais, veuillez réessayer dans :seconds secondes.',
|
||||
'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
|
||||
'mfa_throttle' => 'Trop de tentatives de vérification multifactorielle. Veuillez réessayer dans :secondes secondes.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'S\'inscrire',
|
||||
|
||||
@@ -331,9 +331,9 @@ return [
|
||||
|
||||
// Editor Sidebar
|
||||
'toggle_sidebar' => 'Afficher/masquer la barre latérale',
|
||||
'page_contents' => 'Page Contents',
|
||||
'page_contents_none' => 'No headings were found in the page content.',
|
||||
'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
|
||||
'page_contents' => 'Contenu de la page',
|
||||
'page_contents_none' => 'Aucun titre n\'a été trouvé dans le contenu de la page.',
|
||||
'page_contents_info' => 'Le menu de contenu est généré à partir de tous les formats de titres utilisés dans la page.',
|
||||
'page_tags' => 'Étiquettes de la page',
|
||||
'chapter_tags' => 'Étiquettes du chapitre',
|
||||
'book_tags' => 'Étiquettes du livre',
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'shortcuts_section_navigation' => 'Navigation',
|
||||
'shortcuts_section_actions' => 'Actions communes',
|
||||
'shortcuts_save' => 'Sauvegarder les raccourcis',
|
||||
'shortcuts_overlay_desc' => 'Note : Lorsque les raccourcis sont activés, assistant est disponible en appuyant sur "?" qui mettra en surbrillance les raccourcis disponibles pour les actions actuellement visibles à l\'écran.',
|
||||
'shortcuts_overlay_desc' => 'Note : Lorsque les raccourcis sont activés, assistant est disponible en appuyant sur «?» qui mettra en surbrillance les raccourcis disponibles pour les actions actuellement visibles à l\'écran.',
|
||||
'shortcuts_update_success' => 'Les préférences de raccourci ont été mises à jour !',
|
||||
'shortcuts_overview_desc' => 'Gérer les raccourcis clavier que vous pouvez utiliser pour naviguer dans l\'interface utilisateur du système.',
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ return [
|
||||
'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.',
|
||||
'app_homepage_select' => 'Choisissez une page',
|
||||
'app_footer_links' => 'Liens de pied de page',
|
||||
'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécesittent pas de connexion. Vous pouvez utiliser l\'étiquette "trans::<key>" pour utiliser les traductions définies par le système. Par exemple, utiliser "trans::common.privacy_policy" fournira la traduction de "Politique de Confidentalité" et "trans::common.terms_of_service" fournira la traduction de "Conditions d\'utilisation".',
|
||||
'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécessitent pas de connexion. Vous pouvez utiliser l\'étiquette "trans::<key>" pour utiliser les traductions définies par le système. Par exemple, utiliser "trans::common.privacy_policy" fournira la traduction de "Politique de Confidentalité" et "trans::common.terms_of_service" fournira la traduction de "Conditions d\'utilisation".',
|
||||
'app_footer_links_label' => 'Libellé du lien',
|
||||
'app_footer_links_url' => 'URL du lien',
|
||||
'app_footer_links_add' => 'Ajouter un lien en pied de page',
|
||||
@@ -61,17 +61,17 @@ return [
|
||||
'page_draft_color' => 'Couleur des brouillons',
|
||||
|
||||
// Registration Settings
|
||||
'reg_settings' => 'Préférence pour l\'inscription',
|
||||
'reg_settings' => 'Paramètres d\'inscription',
|
||||
'reg_enable' => 'Activer l\'inscription',
|
||||
'reg_enable_toggle' => 'Activer l\'inscription',
|
||||
'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur pourra s\'enregistrer en tant qu\'utilisateur de l\'application. Lors de l\'inscription, ils se voient attribuer un rôle par défaut.',
|
||||
'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
|
||||
'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur peut s\'inscrire lui-même en tant qu\'utilisateur de l\'application. Lors de son inscription, il se voit attribuer un rôle unique par défaut.',
|
||||
'reg_default_role' => 'Rôle de l\'utilisateur par défaut après l\'inscription',
|
||||
'reg_enable_external_warning' => 'L\'option ci-dessus est ignorée lorsque l\'authentification externe LDAP ou SAML est activée. Les comptes utilisateur pour les membres non existants seront créés automatiquement si l\'authentification, par rapport au système externe utilisé, est réussie.',
|
||||
'reg_email_confirmation' => 'Confirmation de l\'e-mail',
|
||||
'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',
|
||||
'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
|
||||
'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
|
||||
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
|
||||
'reg_confirm_restrict_domain' => 'Restriction de domaine',
|
||||
'reg_confirm_restrict_domain_desc' => 'Indiquez, séparés par des virgules, les domaines de messagerie autorisés pour l\'inscription. Les utilisateurs recevront un e-mail pour confirmer leur adresse avant de pouvoir utiliser l\'application. <br> Notez qu\'ils pourront modifier leur adresse e-mail après leur inscription.',
|
||||
'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place',
|
||||
|
||||
// Sorting Settings
|
||||
@@ -207,7 +207,7 @@ return [
|
||||
'role_all' => 'Tous',
|
||||
'role_own' => 'Propres',
|
||||
'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés',
|
||||
'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
|
||||
'role_controlled_by_page_delete' => 'Contrôlé par les autorisations de suppression de page',
|
||||
'role_save' => 'Enregistrer le rôle',
|
||||
'role_users' => 'Utilisateurs ayant ce rôle',
|
||||
'role_users_none' => 'Aucun utilisateur avec ce rôle actuellement',
|
||||
@@ -264,9 +264,9 @@ return [
|
||||
'users_mfa_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.',
|
||||
'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées',
|
||||
'users_mfa_configure' => 'Méthode de configuration',
|
||||
'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods',
|
||||
'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.',
|
||||
'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?',
|
||||
'users_mfa_reset' => 'Réinitialiser les méthodes d\'authentification multifacteurs',
|
||||
'users_mfa_reset_desc' => 'Cette action réinitialisera et supprimera toutes les méthodes d\'authentification multifacteurs configurées pour cet utilisateur. Si l\'authentification multifacteurs est requise par l\'un de ses rôles, il sera invité à configurer de nouvelles méthodes lors de sa prochaine connexion.',
|
||||
'users_mfa_reset_confirm' => 'Êtes-vous sûr de vouloir réinitialiser l\'authentification multifacteurs pour cet utilisateur ?',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Créer un nouveau jeton API',
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Russe',
|
||||
'sk' => 'Slovaque',
|
||||
'sl' => 'Slovène',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Suédois',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Turc',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
@@ -366,8 +366,9 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'th' => 'Thai',
|
||||
'tr' => 'Türkçe',
|
||||
'uk' => 'Українська',
|
||||
'uz' => 'O‘zbekcha',
|
||||
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
'user_update_notification' => 'Pengguna berhasil diperbarui',
|
||||
'user_delete' => 'pengguna yang dihapus',
|
||||
'user_delete_notification' => 'Pengguna berhasil dihapus',
|
||||
'user_mfa_reset' => 'reset MFA for user',
|
||||
'user_mfa_reset' => 'atur ulang MFA untuk pengguna',
|
||||
'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
|
||||
|
||||
// API Tokens
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
return [
|
||||
// General editor terms
|
||||
'general' => 'Umum',
|
||||
'advanced' => 'Lanjutan',
|
||||
'none' => 'Tidak Ada',
|
||||
'advanced' => 'Tingkat lanjut',
|
||||
'none' => 'Tidak Satupun',
|
||||
'cancel' => 'Batal',
|
||||
'save' => 'Simpan',
|
||||
'close' => 'Tutup',
|
||||
|
||||
@@ -366,6 +366,7 @@ return [
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
'sr' => 'Српски',
|
||||
'sv' => 'Svenska',
|
||||
'th' => 'ภาษาไทย',
|
||||
'tr' => 'Türkçe',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user