Compare commits

..

3 Commits

Author SHA1 Message Date
Dan Brown
fa3b7f77b9 Maintenance: Addressed all phpstan level 5 issues 2026-05-31 15:42:54 +01:00
Dan Brown
03c9ddd14f Maintenance: Further PHPStan level 5 type fixes 2026-05-31 11:12:23 +01:00
Dan Brown
5611c8c0da Maintenance: Started work on PHPStan Level 5 2026-05-30 21:44:25 +01:00
182 changed files with 1602 additions and 2182 deletions

View File

@@ -22,7 +22,7 @@ jobs:
uses: https://github.com/shivammathur/setup-php@v2 uses: https://github.com/shivammathur/setup-php@v2
with: with:
php-version: 8.5 php-version: 8.5
extensions: gd, mbstring, json, curl, xml, dom, mysql, ldap extensions: gd, mbstring, json, curl, xml, mysql, ldap
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache

View File

@@ -23,14 +23,6 @@ jobs:
services: services:
mysql: mysql:
image: docker.io/library/mariadb:12.2.2-noble 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: env:
MARIADB_USER: bookstack-test MARIADB_USER: bookstack-test
MARIADB_PASSWORD: bookstack-test MARIADB_PASSWORD: bookstack-test
@@ -43,7 +35,7 @@ jobs:
uses: https://github.com/shivammathur/setup-php@v2 uses: https://github.com/shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: gd, mbstring, json, curl, xml, dom, mysql, ldap extensions: gd, mbstring, json, curl, xml, mysql, ldap
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache

View File

@@ -21,7 +21,7 @@ jobs:
matrix: matrix:
php: ['8.2', '8.3', '8.4', '8.5'] php: ['8.2', '8.3', '8.4', '8.5']
env: env:
phpextensions: gd, mbstring, json, curl, xml, dom, mysql, ldap, gmp phpextensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
phpextensioncachekey: cache-v1 phpextensioncachekey: cache-v1
steps: steps:
- uses: https://code.forgejo.org/actions/checkout@v6 - uses: https://code.forgejo.org/actions/checkout@v6

View File

@@ -1,33 +0,0 @@
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

View File

@@ -544,7 +544,3 @@ FelixFrizzy :: German
Pedro de Mattia (pdmtt) :: Portuguese, Brazilian Pedro de Mattia (pdmtt) :: Portuguese, Brazilian
lonestan :: Russian lonestan :: Russian
Paul Kernstock (kernstock) :: German Paul Kernstock (kernstock) :: German
brtbr :: German; German Informal
Ricardo Covelo (covelo12) :: Portuguese
Bojan Maksimovic (PolarniMeda) :: Serbian (Cyrillic)
Dian Prawira (wiradian84) :: Indonesian

6
.gitignore vendored
View File

@@ -9,10 +9,10 @@ Homestead.yaml
.idea .idea
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
/public/dist/*.map /public/dist
/public/plugins /public/plugins
/public/css/*.map /public/css
/public/js/*.map /public/js
/public/bower /public/bower
/public/build/ /public/build/
/public/favicon.ico /public/favicon.ico

View File

@@ -8,7 +8,6 @@ use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException; use BookStack\Exceptions\LoginAttemptException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Util\UrlComparison;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@@ -187,8 +186,7 @@ class LoginController extends Controller
{ {
// Store the previous location for redirect after login // Store the previous location for redirect after login
$previous = url()->previous(''); $previous = url()->previous('');
$comparison = new UrlComparison($previous, url('/')); $isPreviousFromInstance = str_starts_with($previous, url('/'));
$isPreviousFromInstance = $comparison->originsMatch() && $comparison->pathsOverlap();
if (!$previous || !setting('app-public') || !$isPreviousFromInstance) { if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
return; return;
} }

View File

@@ -42,7 +42,7 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Retrieve a user by the given credentials. * Retrieve a user by the given credentials.
*/ */
public function retrieveByCredentials(array $credentials): ?Authenticatable public function retrieveByCredentials(array $credentials): ?User
{ {
return User::query() return User::query()
->where('external_auth_id', $credentials['external_auth_id']) ->where('external_auth_id', $credentials['external_auth_id'])

View File

@@ -50,8 +50,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
/** /**
* Create a new authentication guard. * 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->name = $name;
$this->session = $session; $this->session = $session;
$this->provider = $provider; $this->provider = $provider;

View File

@@ -83,6 +83,10 @@ 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 // Sync LDAP groups if required
if ($this->ldapService->shouldSyncGroups()) { if ($this->ldapService->shouldSyncGroups()) {
$this->ldapService->syncGroups($user, $username); $this->ldapService->syncGroups($user, $username);

View File

@@ -131,8 +131,6 @@ class OidcJwtWithClaims implements ProvidesClaims
} }
}, $this->keys); }, $this->keys);
$parsedKeys = array_filter($parsedKeys);
$contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1]; $contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1];
/** @var OidcJwtSigningKey $parsedKey */ /** @var OidcJwtSigningKey $parsedKey */
foreach ($parsedKeys as $parsedKey) { foreach ($parsedKeys as $parsedKey) {

View File

@@ -80,6 +80,7 @@ class OidcService
$provider->setPkceCode($pkceCode); $provider->setPkceCode($pkceCode);
// Try to exchange authorization code for access token // Try to exchange authorization code for access token
/** @var OidcAccessToken $accessToken */
$accessToken = $provider->getAccessToken('authorization_code', [ $accessToken = $provider->getAccessToken('authorization_code', [
'code' => $authorizationCode, 'code' => $authorizationCode,
]); ]);

View File

@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
@@ -27,6 +28,7 @@ class ActivityQueries
*/ */
public function latest(int $count = 20, int $page = 0): array public function latest(int $count = 20, int $page = 0): array
{ {
/** @var Collection<int, Activity> $activityList */
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
@@ -83,6 +85,7 @@ class ActivityQueries
*/ */
public function userActivity(User $user, int $count = 20, int $page = 0): array public function userActivity(User $user, int $count = 20, int $page = 0): array
{ {
/** @var Collection<int, Activity> $activityList */
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')

View File

@@ -59,7 +59,8 @@ class CommentController extends Controller
'html' => ['required', 'string'], 'html' => ['required', 'string'],
]); ]);
$comment = $this->commentRepo->getVisibleById($commentId); $comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
$this->checkOwnablePermission(Permission::CommentUpdate, $comment); $this->checkOwnablePermission(Permission::CommentUpdate, $comment);
$comment = $this->commentRepo->update($comment, $input['html']); $comment = $this->commentRepo->update($comment, $input['html']);
@@ -75,7 +76,8 @@ class CommentController extends Controller
*/ */
public function archive(int $id) public function archive(int $id)
{ {
$comment = $this->commentRepo->getVisibleById($id); $comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) { if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
$this->showPermissionError(); $this->showPermissionError();
} }
@@ -94,7 +96,8 @@ class CommentController extends Controller
*/ */
public function unarchive(int $id) public function unarchive(int $id)
{ {
$comment = $this->commentRepo->getVisibleById($id); $comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) { if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
$this->showPermissionError(); $this->showPermissionError();
} }
@@ -113,7 +116,7 @@ class CommentController extends Controller
*/ */
public function destroy(int $id) public function destroy(int $id)
{ {
$comment = $this->commentRepo->getVisibleById($id); $comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission(Permission::CommentDelete, $comment); $this->checkOwnablePermission(Permission::CommentDelete, $comment);
$this->commentRepo->delete($comment); $this->commentRepo->delete($comment);

View File

@@ -3,6 +3,7 @@
namespace BookStack\Activity\Models; namespace BookStack\Activity\Models;
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\HasCreatorAndUpdater;
@@ -40,6 +41,9 @@ class Comment extends Model implements Loggable, OwnableInterface
/** /**
* Get the entity that this comment belongs to. * 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 public function entity(): MorphTo
{ {
@@ -55,7 +59,9 @@ class Comment extends Model implements Loggable, OwnableInterface
// Ultimately, we could just align the method name to 'commentable' but that would be a potential // 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. // breaking change and not really worthwhile in a patch due to the risk of creating extra problems.
return $this->morphTo(null, 'commentable_type', 'commentable_id'); /** @var MorphTo<Page, $this> $relation */
$relation = $this->morphTo(null, 'commentable_type', 'commentable_id');
return $relation;
} }
/** /**

View File

@@ -73,13 +73,17 @@ class WebhookFormatter
// Load entity owner, creator, updater details // Load entity owner, creator, updater details
$this->addModelFormatter( $this->addModelFormatter(
fn ($event, $model) => ($model instanceof Entity), fn ($event, $model) => ($model instanceof Entity),
fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy']) function ($model) {
$model->load(['ownedBy', 'createdBy', 'updatedBy']);
}
); );
// Load revision detail for page update and create events // Load revision detail for page update and create events
$this->addModelFormatter( $this->addModelFormatter(
fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)), fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)),
fn ($model) => $model->load('currentRevision') function ($model) {
$model->load('currentRevision');
}
); );
} }

View File

@@ -138,8 +138,8 @@ class ApiDocsGenerator
return $validation; return $validation;
} }
if (is_object($validation) && method_exists($validation, '__toString')) { if (is_object($validation) && $validation instanceof \Stringable) {
return strval($validation); return $validation->__toString();
} }
if ($validation instanceof Password) { if ($validation instanceof Password) {

View File

@@ -7,6 +7,7 @@ use BookStack\Theming\ThemeService;
use BookStack\Theming\ThemeViews; use BookStack\Theming\ThemeViews;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\View\FileViewFinder;
class ThemeServiceProvider extends ServiceProvider class ThemeServiceProvider extends ServiceProvider
{ {
@@ -27,7 +28,11 @@ class ThemeServiceProvider extends ServiceProvider
// Boot up the theme system // Boot up the theme system
$themeService = $this->app->make(ThemeService::class); $themeService = $this->app->make(ThemeService::class);
$viewFactory = $this->app->make('view'); $viewFactory = $this->app->make('view');
$themeViews = new ThemeViews($viewFactory->getFinder()); $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);
// Use a custom include so that we can insert theme views before/after includes. // 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 // This is done, even if no theme is active, so that view caching does not create problems

View File

@@ -3,7 +3,6 @@
namespace BookStack\App\Providers; namespace BookStack\App\Providers;
use BookStack\Uploads\ImageService; use BookStack\Uploads\ImageService;
use BookStack\Util\UrlFilter;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@@ -22,8 +21,10 @@ class ValidationRuleServiceProvider extends ServiceProvider
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) { Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
$cleanLinkName = strtolower(trim($value)); $cleanLinkName = strtolower(trim($value));
$filter = new UrlFilter($cleanLinkName); $isJs = str_starts_with($cleanLinkName, 'javascript:');
return $filter->isAllowed(); $isData = str_starts_with($cleanLinkName, 'data:');
return !$isJs && !$isData;
}); });
} }
} }

View File

@@ -6,6 +6,7 @@ use BookStack\Facades\Theme;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
/** /**
@@ -40,7 +41,7 @@ function user(): User
* Check if the current user has a permission. If an ownable element * Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item. * is passed in the jointPermissions are checked against that particular item.
*/ */
function userCan(string|Permission $permission, ?Model $ownable = null): bool function userCan(string|Permission $permission, (Model&OwnableInterface)|null $ownable = null): bool
{ {
if (is_null($ownable)) { if (is_null($ownable)) {
return user()->can($permission); return user()->can($permission);

View File

@@ -7,7 +7,6 @@ use BookStack\Theming\ThemeModule;
use BookStack\Theming\ThemeModuleException; use BookStack\Theming\ThemeModuleException;
use BookStack\Theming\ThemeModuleManager; use BookStack\Theming\ThemeModuleManager;
use BookStack\Theming\ThemeModuleZip; use BookStack\Theming\ThemeModuleZip;
use BookStack\Util\UrlComparison;
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Request;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -200,6 +199,7 @@ class InstallModuleCommand extends Command
{ {
$httpRequests = app()->make(HttpRequestService::class); $httpRequests = app()->make(HttpRequestService::class);
$client = $httpRequests->buildClient(30, ['stream' => true]); $client = $httpRequests->buildClient(30, ['stream' => true]);
$originalUrl = parse_url($location);
$currentLocation = $location; $currentLocation = $location;
$maxRedirects = 3; $maxRedirects = 3;
$redirectCount = 0; $redirectCount = 0;
@@ -212,11 +212,12 @@ class InstallModuleCommand extends Command
if ($statusCode >= 300 && $statusCode < 400 && $redirectCount < $maxRedirects) { if ($statusCode >= 300 && $statusCode < 400 && $redirectCount < $maxRedirects) {
$redirectLocation = $resp->getHeaderLine('Location'); $redirectLocation = $resp->getHeaderLine('Location');
if ($redirectLocation) { if ($redirectLocation) {
$comparison = new UrlComparison($location, $redirectLocation); $redirectUrl = parse_url($redirectLocation);
$redirectOriginMatches = $comparison->originsMatch(); $redirectOriginMatches = ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '')
&& ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '')
&& ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '');
if (!$redirectOriginMatches) { if (!$redirectOriginMatches) {
$redirectUrl = parse_url($redirectLocation);
$redirectOrigin = ($redirectUrl['scheme'] ?? '') . '://' . ($redirectUrl['host'] ?? '') . (isset($redirectUrl['port']) ? ':' . $redirectUrl['port'] : ''); $redirectOrigin = ($redirectUrl['scheme'] ?? '') . '://' . ($redirectUrl['host'] ?? '') . (isset($redirectUrl['port']) ? ':' . $redirectUrl['port'] : '');
$this->info("The download URL is redirecting to a different site: {$redirectOrigin}"); $this->info("The download URL is redirecting to a different site: {$redirectOrigin}");
$shouldContinue = $this->confirm("Do you trust downloading the module from this site?"); $shouldContinue = $this->confirm("Do you trust downloading the module from this site?");

View File

@@ -1,5 +1,7 @@
<?php <?php
declare(strict_types=1);
namespace BookStack\Entities\Controllers; namespace BookStack\Entities\Controllers;
use BookStack\Activity\Tools\CommentTree; use BookStack\Activity\Tools\CommentTree;
@@ -10,7 +12,9 @@ use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController; use BookStack\Http\ApiController;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use Exception; use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
class PageApiController extends ApiController class PageApiController extends ApiController
{ {
@@ -45,7 +49,7 @@ class PageApiController extends ApiController
/** /**
* Get a listing of pages visible to the user. * Get a listing of pages visible to the user.
*/ */
public function list() public function list(): JsonResponse
{ {
$pages = $this->queries->visibleForList() $pages = $this->queries->visibleForList()
->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']); ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']);
@@ -69,7 +73,7 @@ class PageApiController extends ApiController
* Any images included via base64 data URIs will be extracted and saved as gallery * Any images included via base64 data URIs will be extracted and saved as gallery
* images against the page during upload. * images against the page during upload.
*/ */
public function create(Request $request) public function create(Request $request): JsonResponse
{ {
$this->validate($request, $this->rules['create']); $this->validate($request, $this->rules['create']);
@@ -102,9 +106,9 @@ class PageApiController extends ApiController
* Comments for the page are provided in a tree-structure representing the hierarchy of top-level * 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. * comments and replies, for both archived and active comments.
*/ */
public function read(string $id) public function read(string $id): JsonResponse
{ {
$page = $this->queries->findVisibleByIdOrFail($id); $page = $this->queries->findVisibleByIdOrFail(intval($id));
$page = $page->forJsonDisplay(); $page = $page->forJsonDisplay();
$commentTree = (new CommentTree($page)); $commentTree = (new CommentTree($page));
@@ -124,11 +128,11 @@ class PageApiController extends ApiController
* Providing a 'book_id' or 'chapter_id' property will essentially move * Providing a 'book_id' or 'chapter_id' property will essentially move
* the page into that parent element if you have permissions to do so. * the page into that parent element if you have permissions to do so.
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id): JsonResponse
{ {
$requestData = $this->validate($request, $this->rules['update']); $requestData = $this->validate($request, $this->rules['update']);
$page = $this->queries->findVisibleByIdOrFail($id); $page = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::PageUpdate, $page); $this->checkOwnablePermission(Permission::PageUpdate, $page);
$parent = null; $parent = null;
@@ -161,9 +165,9 @@ class PageApiController extends ApiController
* Delete a page. * Delete a page.
* This will typically send the page to the recycle bin. * This will typically send the page to the recycle bin.
*/ */
public function delete(string $id) public function delete(string $id): Response
{ {
$page = $this->queries->findVisibleByIdOrFail($id); $page = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission(Permission::PageDelete, $page); $this->checkOwnablePermission(Permission::PageDelete, $page);
$this->pageRepo->destroy($page); $this->pageRepo->destroy($page);

View File

@@ -25,6 +25,7 @@ use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlContentFilterConfig; use BookStack\Util\HtmlContentFilterConfig;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Throwable; use Throwable;
@@ -358,8 +359,8 @@ class PageController extends Controller
*/ */
public function showRecentlyUpdated() public function showRecentlyUpdated()
{ {
$visibleBelongsScope = function (BelongsTo $query) { $visibleBelongsScope = function (Relation $relation): void {
$query->scopes('visible'); $relation->scopes('visible');
}; };
$pages = $this->queries->visibleForList() $pages = $this->queries->visibleForList()

View File

@@ -177,7 +177,7 @@ class PageRevisionController extends Controller
*/ */
public function destroyUserDraft(string $pageId) public function destroyUserDraft(string $pageId)
{ {
$page = $this->pageQueries->findVisibleByIdOrFail($pageId); $page = $this->pageQueries->findVisibleByIdOrFail(intval($pageId));
$this->revisionRepo->deleteDraftsForCurrentUser($page); $this->revisionRepo->deleteDraftsForCurrentUser($page);
return response('', 200); return response('', 200);

View File

@@ -20,10 +20,10 @@ use Illuminate\Support\Collection;
* @property ?int $image_id * @property ?int $image_id
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?int $sort_rule_id * @property ?int $sort_rule_id
* @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection<int, Chapter> $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages * @property \Illuminate\Database\Eloquent\Collection<int, Page> $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages * @property \Illuminate\Database\Eloquent\Collection<int, Page> $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves * @property \Illuminate\Database\Eloquent\Collection<int, Bookshelf> $shelves
* @property ?SortRule $sortRule * @property ?SortRule $sortRule
*/ */
class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface

View File

@@ -46,7 +46,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $created_by * @property int|null $created_by
* @property int|null $updated_by * @property int|null $updated_by
* @property int|null $owned_by * @property int|null $owned_by
* @property Collection $tags * @property Collection<int, Tag> $tags
* *
* @method static Entity|Builder visible() * @method static Entity|Builder visible()
* @method static Builder withLastView() * @method static Builder withLastView()

View File

@@ -24,8 +24,8 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property int $revision_count * @property int $revision_count
* @property string $editor * @property string $editor
* @property Chapter|null $chapter * @property Chapter|null $chapter
* @property Collection $attachments * @property Collection<int, Attachment> $attachments
* @property Collection $revisions * @property Collection<int, PageRevision> $revisions
* @property PageRevision $currentRevision * @property PageRevision $currentRevision
*/ */
class Page extends BookChild class Page extends BookChild

View File

@@ -6,6 +6,7 @@ use BookStack\Activity\Models\View;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -20,6 +21,7 @@ class QueryPopular
public function run(int $count, int $page, array $filterModels): Collection public function run(int $count, int $page, array $filterModels): Collection
{ {
/** @var Builder<View> $query */
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type') ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count')) ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))

View File

@@ -5,6 +5,7 @@ namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\View; use BookStack\Activity\Models\View;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class QueryRecentlyViewed class QueryRecentlyViewed
@@ -22,6 +23,7 @@ class QueryRecentlyViewed
return collect(); return collect();
} }
/** @var Builder<View> $query */
$query = $this->permissions->restrictEntityRelationQuery( $query = $this->permissions->restrictEntityRelationQuery(
View::query(), View::query(),
'views', 'views',

View File

@@ -5,6 +5,7 @@ namespace BookStack\Entities\Queries;
use BookStack\Activity\Models\Favourite; use BookStack\Activity\Models\Favourite;
use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
class QueryTopFavourites class QueryTopFavourites
@@ -22,6 +23,7 @@ class QueryTopFavourites
return collect(); return collect();
} }
/** @var Builder<Favourite> $query */
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type') ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type')
->select('favourites.*') ->select('favourites.*')

View File

@@ -25,9 +25,13 @@ class CustomListItemRenderer implements NodeRendererInterface
*/ */
public function render(Node $node, ChildNodeRendererInterface $childRenderer) public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{ {
if (!($node instanceof ListItem)) {
return null;
}
$listItem = $this->baseRenderer->render($node, $childRenderer); $listItem = $this->baseRenderer->render($node, $childRenderer);
if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) { if ($this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
$listItem->setAttribute('class', 'task-list-item'); $listItem->setAttribute('class', 'task-list-item');
} }

View File

@@ -421,28 +421,41 @@ class PageContent
*/ */
protected function headerNodesToLevelList(DOMNodeList $nodeList): array protected function headerNodesToLevelList(DOMNodeList $nodeList): array
{ {
$tree = collect($nodeList)->map(function (DOMElement $header) { $minLevel = 6;
$headerDetails = array_map(function (DOMNode $header) use (&$minLevel) {
if (!$header instanceof DOMElement) {
return null;
}
$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);
if (empty($text)) {
return null;
}
$level = intval(str_replace('h', '', $header->nodeName));
if ($level < $minLevel) {
$minLevel = $level;
}
return [ return [
'nodeName' => strtolower($header->nodeName), 'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)), 'level' => $level,
'link' => '#' . $header->getAttribute('id'), 'link' => '#' . $header->getAttribute('id'),
'text' => $text, 'text' => $text,
]; ];
})->filter(function ($header) { }, [...$nodeList]);
return mb_strlen($header['text']) > 0;
}); $filtered = array_values(array_filter($headerDetails));
// Shift headers if only smaller headers have been used // Shift headers if only smaller headers have been used
$levelChange = ($tree->pluck('level')->min() - 1); $levelChange = ($minLevel - 1);
$tree = $tree->map(function ($header) use ($levelChange) { foreach ($filtered as $index => $header) {
$header['level'] -= ($levelChange); $filtered[$index]['level'] -= $levelChange;
}
return $header; return $filtered;
});
return $tree->toArray();
} }
} }

View File

@@ -192,10 +192,10 @@ class PageIncludeParser
/** /**
* Get the parent paragraph of the given node, if existing. * Get the parent paragraph of the given node, if existing.
*/ */
protected function getParentParagraph(DOMNode $parent): ?DOMNode protected function getParentParagraph(DOMNode $parent): ?DOMElement
{ {
do { do {
if (strtolower($parent->nodeName) === 'p') { if (strtolower($parent->nodeName) === 'p' && $parent instanceof DOMElement) {
return $parent; return $parent;
} }

View File

@@ -52,7 +52,7 @@ class SlugGenerator
{ {
$slug = Str::slug($name); $slug = Str::slug($name);
if ($slug === '') { if ($slug === '') {
$slug = substr(md5(rand(1, 500)), 0, 5); $slug = substr(md5(strval(rand(1, 500))), 0, 5);
} }
return $slug; return $slug;

View File

@@ -25,7 +25,6 @@ class Handler extends ExceptionHandler
protected $dontReport = [ protected $dontReport = [
NotFoundException::class, NotFoundException::class,
StoppedAuthenticationException::class, StoppedAuthenticationException::class,
NotifyException::class,
]; ];
/** /**

View File

@@ -6,22 +6,18 @@ use Exception;
use Illuminate\Contracts\Support\Responsable; use Illuminate\Contracts\Support\Responsable;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; 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 class NotifyException extends Exception implements Responsable, HttpExceptionInterface
{ {
public function __construct( public $message;
string $message, public string $redirectLocation;
public string $redirectLocation = '/', protected int $status;
protected int $status = 500
) { public function __construct(string $message, string $redirectLocation = '/', int $status = 500)
{
$this->message = $message; $this->message = $message;
$this->redirectLocation = $redirectLocation;
$this->status = $status;
parent::__construct(); parent::__construct();
} }

View File

@@ -3,6 +3,8 @@
namespace BookStack\Exports; namespace BookStack\Exports;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
@@ -119,6 +121,9 @@ class ImportRepo
$parentModel = null; $parentModel = null;
if ($import->type === 'page' || $import->type === 'chapter') { if ($import->type === 'page' || $import->type === 'chapter') {
$parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null; $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(); DB::beginTransaction();

View File

@@ -48,7 +48,7 @@ class ZipImportRunner
* Returns the top-level entity item which was imported. * Returns the top-level entity item which was imported.
* @throws ZipImportException * @throws ZipImportException
*/ */
public function run(Import $import, ?Entity $parent = null): Entity public function run(Import $import, Book|Chapter|null $parent = null): Entity
{ {
$zipPath = $this->getZipPath($import); $zipPath = $this->getZipPath($import);
$reader = new ZipExportReader($zipPath); $reader = new ZipExportReader($zipPath);

View File

@@ -7,6 +7,7 @@ use BookStack\App\Model;
use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Users\Models\OwnableInterface;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -80,7 +81,7 @@ abstract class Controller extends BaseController
/** /**
* Check the current user's permissions against an ownable item otherwise throw an exception. * Check the current user's permissions against an ownable item otherwise throw an exception.
*/ */
protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void protected function checkOwnablePermission(string|Permission $permission, Model&OwnableInterface $ownable, string $redirectLocation = '/'): void
{ {
if (!userCan($permission, $ownable)) { if (!userCan($permission, $ownable)) {
$this->showPermissionError($redirectLocation); $this->showPermissionError($redirectLocation);

View File

@@ -101,6 +101,7 @@ class JointPermissionBuilder
/** /**
* Get a query for fetching a book with its children. * Get a query for fetching a book with its children.
* @return Builder<Book>
*/ */
protected function bookFetchQuery(): Builder protected function bookFetchQuery(): Builder
{ {
@@ -117,9 +118,11 @@ class JointPermissionBuilder
/** /**
* Build joint permissions for the given book and role combinations. * 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 protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void
{ {
/** @var EloquentCollection<int, Entity> $entities */
$entities = clone $books; $entities = clone $books;
/** @var Book $book */ /** @var Book $book */

View File

@@ -2,8 +2,8 @@
namespace BookStack\References; namespace BookStack\References;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;

View File

@@ -22,6 +22,7 @@ class ReferenceFetcher
*/ */
public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection
{ {
/** @var Collection<int, Reference> $references */
$references = $this->queryReferencesToEntity($entity)->get(); $references = $this->queryReferencesToEntity($entity)->get();
$this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents); $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents);
@@ -37,6 +38,9 @@ class ReferenceFetcher
return $this->queryReferencesToEntity($entity)->count(); return $this->queryReferencesToEntity($entity)->count();
} }
/**
* @return Builder<Reference>
*/
protected function queryReferencesToEntity(Entity $entity): Builder protected function queryReferencesToEntity(Entity $entity): Builder
{ {
$baseQuery = Reference::query() $baseQuery = Reference::query()

View File

@@ -4,6 +4,7 @@ namespace BookStack\Search;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\EntityTable;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Tools\EntityHydrator; use BookStack\Entities\Tools\EntityHydrator;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
@@ -90,6 +91,8 @@ class SearchRunner
/** /**
* Get a page of result data from the given query based on the provided page parameters. * 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 protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection
{ {
@@ -106,6 +109,7 @@ class SearchRunner
/** /**
* Create a search query for an entity. * Create a search query for an entity.
* @param string[] $entityTypes * @param string[] $entityTypes
* @return EloquentBuilder<EntityTable>
*/ */
protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder
{ {
@@ -290,7 +294,7 @@ class SearchRunner
$query->where('name', '=', $tagParts['name']); $query->where('name', '=', $tagParts['name']);
} }
if (is_numeric($tagParts['value']) && is_finite($tagParts['value']) && $tagParts['operator'] !== 'like') { if (is_numeric($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 // 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.

View File

@@ -243,9 +243,9 @@ class SettingService
/** /**
* Convert a setting key into a user-specific key. * Convert a setting key into a user-specific key.
*/ */
protected function userKey(string $userId, string $key = ''): string protected function userKey(int $userId, string $key = ''): string
{ {
return 'user:' . $userId . ':' . $key; return 'user:' . strval($userId) . ':' . $key;
} }
/** /**
@@ -267,7 +267,7 @@ class SettingService
/** /**
* Delete settings for a given user id. * Delete settings for a given user id.
*/ */
public function deleteUserSettings(string $userId): void public function deleteUserSettings(int $userId): void
{ {
Setting::query() Setting::query()
->where('setting_key', 'like', $this->userKey($userId) . '%') ->where('setting_key', 'like', $this->userKey($userId) . '%')

View File

@@ -4,12 +4,37 @@ namespace BookStack\Sorting;
class BookSortMapItem class BookSortMapItem
{ {
public function __construct( /**
public int $id, * @var int
public int $sort, */
public int|null $parentChapterId, public $id;
public string $type,
public int $parentBookId, /**
) { * @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;
} }
} }

View File

@@ -168,7 +168,7 @@ class BookSorter
$model->priority = $sortMapItem->sort; $model->priority = $sortMapItem->sort;
} }
if ($priorityChanged || $chapterChanged) { if ($chapterChanged || $priorityChanged) {
$model::withoutTimestamps(fn () => $model->save()); $model::withoutTimestamps(fn () => $model->save());
} }
} }

View File

@@ -38,21 +38,35 @@ class SortRule extends Model implements Loggable
$this->sequence = implode(',', $values); $this->sequence = implode(',', $values);
} }
/**
* @inheritDoc
*/
public function logDescriptor(): string public function logDescriptor(): string
{ {
return "({$this->id}) {$this->name}"; return "({$this->id}) {$this->name}";
} }
/**
* Get the URL to where this rule can be managed.
*/
public function getUrl(): string public function getUrl(): string
{ {
return url("/settings/sorting/rules/{$this->id}"); 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 public function books(): HasMany
{ {
return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id'); 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 public static function allByName(): Collection
{ {
return static::query() return static::query()

View File

@@ -10,7 +10,6 @@ use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Users\Models\OwnableInterface; use BookStack\Users\Models\OwnableInterface;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
use BookStack\Util\UrlFilter;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -72,7 +71,7 @@ class Attachment extends Model implements OwnableInterface
public function getUrl($openInline = false): string public function getUrl($openInline = false): string
{ {
if ($this->external && !str_starts_with($this->path, 'http')) { if ($this->external && !str_starts_with($this->path, 'http')) {
return (new UrlFilter($this->path))->clean(); return $this->path;
} }
return url('/attachments/' . $this->id . ($openInline ? '?open=true' : '')); return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));

View File

@@ -102,7 +102,7 @@ class AttachmentService
/** /**
* Updates the ordering for a listing of attached files. * Updates the ordering for a listing of attached files.
*/ */
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId) public function updateFileOrderWithinPage(array $attachmentOrder, int $pageId)
{ {
foreach ($attachmentOrder as $index => $attachmentId) { foreach ($attachmentOrder as $index => $attachmentId) {
Attachment::query()->where('uploaded_to', '=', $pageId) Attachment::query()->where('uploaded_to', '=', $pageId)

View File

@@ -11,7 +11,6 @@ use BookStack\Http\Controller;
use BookStack\Permissions\Permission; use BookStack\Permissions\Permission;
use BookStack\Uploads\Attachment; use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService; use BookStack\Uploads\AttachmentService;
use BookStack\Util\UrlFilter;
use Exception; use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -108,9 +107,6 @@ class AttachmentController extends Controller
{ {
/** @var Attachment $attachment */ /** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId); $attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission(Permission::PageView, $attachment->page);
$this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
try { try {
$this->validate($request, [ $this->validate($request, [
@@ -124,6 +120,10 @@ class AttachmentController extends Controller
]), 422); ]), 422);
} }
$this->checkOwnablePermission(Permission::PageView, $attachment->page);
$this->checkOwnablePermission(Permission::PageUpdate, $attachment->page);
$this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment);
$attachment = $this->attachmentService->updateFile($attachment, [ $attachment = $this->attachmentService->updateFile($attachment, [
'name' => $request->input('attachment_edit_name'), 'name' => $request->input('attachment_edit_name'),
'link' => $request->input('attachment_edit_url'), 'link' => $request->input('attachment_edit_url'),
@@ -142,10 +142,6 @@ class AttachmentController extends Controller
public function attachLink(Request $request) public function attachLink(Request $request)
{ {
$pageId = $request->input('attachment_link_uploaded_to'); $pageId = $request->input('attachment_link_uploaded_to');
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkPermission(Permission::AttachmentCreateAll);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
try { try {
$this->validate($request, [ $this->validate($request, [
@@ -160,6 +156,11 @@ class AttachmentController extends Controller
]), 422); ]), 422);
} }
$page = $this->pageQueries->findVisibleByIdOrFail($pageId);
$this->checkPermission(Permission::AttachmentCreateAll);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$attachmentName = $request->input('attachment_link_name'); $attachmentName = $request->input('attachment_link_name');
$link = $request->input('attachment_link_url'); $link = $request->input('attachment_link_url');
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId)); $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
@@ -222,8 +223,7 @@ class AttachmentController extends Controller
} }
if ($attachment->external) { if ($attachment->external) {
$url = (new UrlFilter($attachment->path))->clean(); return redirect($attachment->path);
return redirect($url);
} }
$fileName = $attachment->getFileName(); $fileName = $attachment->getFileName();

View File

@@ -87,7 +87,7 @@ class RoleApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById(intval($id));
$this->singleFormatter($role); $this->singleFormatter($role);
return response()->json($role); return response()->json($role);

View File

@@ -94,7 +94,7 @@ class RoleController extends Controller
public function edit(string $id) public function edit(string $id)
{ {
$this->checkPermission(Permission::UserRolesManage); $this->checkPermission(Permission::UserRolesManage);
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById(intval($id));
$this->setPageTitle(trans('settings.role_edit')); $this->setPageTitle(trans('settings.role_edit'));
@@ -129,7 +129,7 @@ class RoleController extends Controller
public function showDelete(string $id) public function showDelete(string $id)
{ {
$this->checkPermission(Permission::UserRolesManage); $this->checkPermission(Permission::UserRolesManage);
$role = $this->permissionsRepo->getRoleById($id); $role = $this->permissionsRepo->getRoleById(intval($id));
$roles = $this->permissionsRepo->getAllRolesExcept($role); $roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole); $roles->prepend($blankRole);
@@ -151,7 +151,7 @@ class RoleController extends Controller
try { try {
$migrateRoleId = intval($request->input('migrate_role_id') ?: "0"); $migrateRoleId = intval($request->input('migrate_role_id') ?: "0");
$this->permissionsRepo->deleteRole($id, $migrateRoleId); $this->permissionsRepo->deleteRole(intval($id), $migrateRoleId);
} catch (PermissionsException $e) { } catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage()); $this->showErrorNotification($e->getMessage());

View File

@@ -110,7 +110,7 @@ class UserApiController extends ApiController
*/ */
public function read(string $id) public function read(string $id)
{ {
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById(intval($id));
$this->singleFormatter($user); $this->singleFormatter($user);
return response()->json($user); return response()->json($user);
@@ -124,8 +124,8 @@ class UserApiController extends ApiController
*/ */
public function update(Request $request, string $id) public function update(Request $request, string $id)
{ {
$data = $this->validate($request, $this->rules($id)['update']); $data = $this->validate($request, $this->rules(intval($id))['update']);
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById(intval($id));
$this->userRepo->update($user, $data, userCan(Permission::UsersManage)); $this->userRepo->update($user, $data, userCan(Permission::UsersManage));
$this->singleFormatter($user); $this->singleFormatter($user);
@@ -140,7 +140,7 @@ class UserApiController extends ApiController
*/ */
public function delete(Request $request, string $id) public function delete(Request $request, string $id)
{ {
$user = $this->userRepo->getById($id); $user = $this->userRepo->getById(intval($id));
$newOwnerId = $request->input('migrate_ownership_id', null); $newOwnerId = $request->input('migrate_ownership_id', null);
$this->userRepo->destroy($user, $newOwnerId); $this->userRepo->destroy($user, $newOwnerId);

View File

@@ -1,16 +1,13 @@
<?php <?php
namespace BookStack\Util\HtmlPurifier; namespace BookStack\Util;
use BookStack\App\AppVersion; use BookStack\App\AppVersion;
use BookStack\Util\HtmlPurifier\Filters\UriLimitFileProtocolToAnchors;
use BookStack\Util\UrlFilter;
use HTMLPurifier; use HTMLPurifier;
use HTMLPurifier_Config; use HTMLPurifier_Config;
use HTMLPurifier_DefinitionCache_Serializer; use HTMLPurifier_DefinitionCache_Serializer;
use HTMLPurifier_HTML5Config; use HTMLPurifier_HTML5Config;
use HTMLPurifier_HTMLDefinition; use HTMLPurifier_HTMLDefinition;
use HTMLPurifier_URIDefinition;
/** /**
* Provides a configured HTML Purifier instance. * Provides a configured HTML Purifier instance.
@@ -36,12 +33,7 @@ class ConfiguredHtmlPurifier
$htmlDef = $config->getDefinition('HTML', true, true); $htmlDef = $config->getDefinition('HTML', true, true);
if ($htmlDef instanceof HTMLPurifier_HTMLDefinition) { if ($htmlDef instanceof HTMLPurifier_HTMLDefinition) {
$this->configureHtmlDefinition($htmlDef); $this->configureDefinition($htmlDef);
}
$uriDef = $config->getDefinition('URI', true, true);
if ($uriDef instanceof HTMLPurifier_URIDefinition) {
$this->configureUriDefinition($uriDef);
} }
$this->purifier = new HTMLPurifier($config); $this->purifier = new HTMLPurifier($config);
@@ -85,18 +77,21 @@ class ConfiguredHtmlPurifier
$config->set('Attr.ID.HTML5', true); $config->set('Attr.ID.HTML5', true);
$config->set('Output.FixInnerHTML', false); $config->set('Output.FixInnerHTML', false);
$config->set('URI.SafeIframeRegexp', '%^(http://|https://|//)%'); $config->set('URI.SafeIframeRegexp', '%^(http://|https://|//)%');
$config->set('URI.AllowedSchemes', [
$allowedSchemes = UrlFilter::getAllowedSchemes(); 'http' => true,
$allowedSchemesSetting = []; 'https' => true,
foreach ($allowedSchemes as $scheme) { 'mailto' => true,
$allowedSchemesSetting[$scheme] = true; 'ftp' => true,
} 'nntp' => true,
$config->set('URI.AllowedSchemes', $allowedSchemesSetting); 'news' => true,
'tel' => true,
'file' => true,
]);
// $config->set('Cache.DefinitionImpl', null); // Disable cache during testing // $config->set('Cache.DefinitionImpl', null); // Disable cache during testing
} }
protected function configureHtmlDefinition(HTMLPurifier_HTMLDefinition $definition): void public function configureDefinition(HTMLPurifier_HTMLDefinition $definition): void
{ {
// Allow the object element // Allow the object element
$definition->addElement( $definition->addElement(
@@ -154,15 +149,6 @@ class ConfiguredHtmlPurifier
// Allow mention-ids on links // Allow mention-ids on links
$definition->addAttribute('a', 'data-mention-user-id', 'Number'); $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 public function purify(string $html): string

View File

@@ -3,6 +3,7 @@
namespace BookStack\Util; namespace BookStack\Util;
use Closure; use Closure;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Throwable; use Throwable;
@@ -24,7 +25,7 @@ use Throwable;
class DatabaseTransaction class DatabaseTransaction
{ {
/** /**
* @param (Closure(static): TReturn) $callback * @param (Closure(Connection): TReturn) $callback
*/ */
public function __construct( public function __construct(
protected Closure $callback protected Closure $callback

View File

@@ -2,7 +2,6 @@
namespace BookStack\Util; namespace BookStack\Util;
use BookStack\Util\HtmlPurifier\ConfiguredHtmlPurifier;
use DOMAttr; use DOMAttr;
use DOMElement; use DOMElement;
use DOMNodeList; use DOMNodeList;

View File

@@ -1,53 +0,0 @@
<?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;
}
}

View File

@@ -1,26 +0,0 @@
<?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;
}
}

View File

@@ -1,36 +0,0 @@
<?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);
}
}

View File

@@ -1,103 +0,0 @@
<?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

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
1021b927781747de28641b5135ae31befef7c9aedc82de9178d6d412b1327d97 22e02ee72d21ff719c1073abbec8302f8e2096ba6d072e133051064ed24b45b1

View File

@@ -482,8 +482,8 @@ electron-to-chromium
License: ISC License: ISC
License File: node_modules/electron-to-chromium/LICENSE License File: node_modules/electron-to-chromium/LICENSE
Copyright: Copyright 2018 Kilian Valkhof Copyright: Copyright 2018 Kilian Valkhof
Source: 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 Link: git+https://github.com/kilian/electron-to-chromium.git
----------- -----------
emittery emittery
License: MIT License: MIT
@@ -860,7 +860,7 @@ License: MIT
License File: node_modules/handlebars/LICENSE License File: node_modules/handlebars/LICENSE
Copyright: Copyright (C) 2011-2019 by Yehuda Katz Copyright: Copyright (C) 2011-2019 by Yehuda Katz
Source: https://github.com/handlebars-lang/handlebars.js.git Source: https://github.com/handlebars-lang/handlebars.js.git
Link: https://handlebarsjs.com/ Link: https://www.handlebarsjs.com/
----------- -----------
has-bigints has-bigints
License: MIT License: MIT
@@ -1663,6 +1663,13 @@ Copyright: Copyright (c) Stephen Sugden <**@*************.***> (stephensugden.co
Source: grncdr/merge-stream Source: grncdr/merge-stream
Link: 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 mimic-fn
License: MIT License: MIT
License File: node_modules/mimic-fn/license License File: node_modules/mimic-fn/license
@@ -3567,6 +3574,13 @@ Copyright: Copyright (c) 2017-present Devon Govett
Source: https://github.com/parcel-bundler/watcher.git Source: https://github.com/parcel-bundler/watcher.git
Link: 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 @parcel/watcher
License: MIT License: MIT
License File: node_modules/@parcel/watcher/LICENSE License File: node_modules/@parcel/watcher/LICENSE
@@ -3589,8 +3603,8 @@ Link: https://github.com/un-ts/pkgr/blob/master/packages/core
License: MIT License: MIT
License File: node_modules/@sinclair/typebox/license License File: node_modules/@sinclair/typebox/license
Copyright: Copyright (c) 2017-2026 Haydn Paterson Copyright: Copyright (c) 2017-2026 Haydn Paterson
Source: https://github.com/sinclairzx81/sinclair-typebox Source: https://github.com/sinclairzx81/typebox-legacy
Link: https://github.com/sinclairzx81/sinclair-typebox Link: https://github.com/sinclairzx81/typebox-legacy
----------- -----------
@sinonjs/commons @sinonjs/commons
License: BSD-3-Clause License: BSD-3-Clause
@@ -3803,3 +3817,8 @@ Link: https://github.com/ungap/structured-clone#readme
License: MIT License: MIT
Source: git+https://github.com/unrs/unrs-resolver.git Source: git+https://github.com/unrs/unrs-resolver.git
Link: https://github.com/unrs/unrs-resolver#readme 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

View File

@@ -110,7 +110,7 @@ License: BSD-3-Clause
License File: vendor/firebase/php-jwt/LICENSE License File: vendor/firebase/php-jwt/LICENSE
Copyright: Copyright (c) 2011, Neuman Vong Copyright: Copyright (c) 2011, Neuman Vong
Source: https://github.com/googleapis/php-jwt.git Source: https://github.com/googleapis/php-jwt.git
Link: https://github.com/googleapis/php-jwt Link: https://github.com/firebase/php-jwt
----------- -----------
fruitcake/php-cors fruitcake/php-cors
License: MIT License: MIT

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -99,8 +99,8 @@ return [
'user_update_notification' => 'Uživatel byl úspěšně aktualizován', 'user_update_notification' => 'Uživatel byl úspěšně aktualizován',
'user_delete' => 'odstranil uživatele', 'user_delete' => 'odstranil uživatele',
'user_delete_notification' => 'Uživatel byl úspěšně odstraněn', 'user_delete_notification' => 'Uživatel byl úspěšně odstraněn',
'user_mfa_reset' => 'obnovit vícefaktorové ověření pro uživatele', 'user_mfa_reset' => 'reset MFA for user',
'user_mfa_reset_notification' => 'Obnovení metod vícefaktorového ověřování', 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
// API Tokens // API Tokens
'api_token_create' => 'API token byl vytvořen', 'api_token_create' => 'API token byl vytvořen',

View File

@@ -8,7 +8,7 @@ return [
'failed' => 'Neplatné přihlašovací údaje.', 'failed' => 'Neplatné přihlašovací údaje.',
'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.', 'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.',
'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.', 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
// Login & Register // Login & Register
'sign_up' => 'Registrace', 'sign_up' => 'Registrace',

View File

@@ -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_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' => 'Možnost automatického řazení',
'books_sort_auto_sort_active' => 'Aktivní automatické řazení: :sortName', 'books_sort_auto_sort_active' => 'Aktivní automatické řazení: :sortName',
'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_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_named' => 'Seřadit knihu :bookName', 'books_sort_named' => 'Seřadit knihu :bookName',
'books_sort_name' => 'Seřadit podle názvu', 'books_sort_name' => 'Seřadit podle názvu',
'books_sort_created' => 'Seřadit podle data vytvoření', 'books_sort_created' => 'Seřadit podle data vytvoření',
@@ -331,9 +331,9 @@ return [
// Editor Sidebar // Editor Sidebar
'toggle_sidebar' => 'Skrýt/Zobrazit postranní panel', 'toggle_sidebar' => 'Skrýt/Zobrazit postranní panel',
'page_contents' => 'Obsah stránky', 'page_contents' => 'Page Contents',
'page_contents_none' => 'Na stránce nejsou žádné nadpisy.', 'page_contents_none' => 'No headings were found in the page content.',
'page_contents_info' => 'Obsah se generuje ze všech použitých nadpisů na stránce.', 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
'page_tags' => 'Štítky stránky', 'page_tags' => 'Štítky stránky',
'chapter_tags' => 'Štítky kapitoly', 'chapter_tags' => 'Štítky kapitoly',
'book_tags' => 'Štítky knihy', 'book_tags' => 'Štítky knihy',

View File

@@ -207,7 +207,7 @@ return [
'role_all' => 'Vše', 'role_all' => 'Vše',
'role_own' => 'Vlastní', 'role_own' => 'Vlastní',
'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány', 'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',
'role_controlled_by_page_delete' => 'Řídí se právem k odstranění stránky', 'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
'role_save' => 'Uložit roli', 'role_save' => 'Uložit roli',
'role_users' => 'Uživatelé mající tuto roli', 'role_users' => 'Uživatelé mající tuto roli',
'role_users_none' => 'Žádný uživatel nemá 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_create' => 'Vytvořit Token',
'users_api_tokens_expires' => 'Vyprší', 'users_api_tokens_expires' => 'Vyprší',
'users_api_tokens_docs' => 'Dokumentace API', 'users_api_tokens_docs' => 'Dokumentace API',
'users_mfa' => 'Vícefaktorové ověření', 'users_mfa' => 'Vícefázové ověření',
'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.', '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_x_methods' => ':count nastavená metoda|:count nastavených metod',
'users_mfa_configure' => 'Konfigurovat metody', 'users_mfa_configure' => 'Konfigurovat metody',
'users_mfa_reset' => 'Obnova metod vícefaktorového ověření', 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods',
'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_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' => 'Opravdu chcete obnovit vícefaktorové ověřování pro tohoto uživatele?', 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?',
// API Tokens // API Tokens
'user_api_token_create' => 'Vytvořit API Token', 'user_api_token_create' => 'Vytvořit API Token',
@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -8,7 +8,7 @@ return [
'failed' => 'De indtastede brugeroplysninger stemmer ikke overens med vores registreringer.', 'failed' => 'De indtastede brugeroplysninger stemmer ikke overens med vores registreringer.',
'throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.', 'throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.',
'mfa_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.',
// Login & Register // Login & Register
'sign_up' => 'Registrer', 'sign_up' => 'Registrer',

View File

@@ -331,9 +331,9 @@ return [
// Editor Sidebar // Editor Sidebar
'toggle_sidebar' => 'Sidebjælke til/fra', 'toggle_sidebar' => 'Sidebjælke til/fra',
'page_contents' => 'Sideindhold', 'page_contents' => 'Page Contents',
'page_contents_none' => 'Ingen overskrifter blev fundet i sidens indhold.', 'page_contents_none' => 'No headings were found in the page content.',
'page_contents_info' => 'Indholdsmenuen er genereret fra alle kursformater, der bruges på siden.', 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
'page_tags' => 'Sidetags', 'page_tags' => 'Sidetags',
'chapter_tags' => 'Kapiteltags', 'chapter_tags' => 'Kapiteltags',
'book_tags' => 'Bogtags', 'book_tags' => 'Bogtags',

View File

@@ -366,9 +366,8 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'Thailandsk', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',
'uk' => 'Українська', 'uk' => 'Українська',
'uz' => 'Ozbekcha', 'uz' => 'Ozbekcha',

View File

@@ -8,7 +8,7 @@ return [
'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.', 'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.',
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.',
'mfa_throttle' => 'Zu viele Multi-Faktor-Verifizierungsversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
// Login & Register // Login & Register
'sign_up' => 'Registrieren', 'sign_up' => 'Registrieren',

View File

@@ -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_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' => 'Automatische Sortierfunktionsoption',
'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName', 'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName',
'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_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_named' => 'Buch ":bookName" sortieren', 'books_sort_named' => 'Buch ":bookName" sortieren',
'books_sort_name' => 'Sortieren nach Namen', 'books_sort_name' => 'Sortieren nach Namen',
'books_sort_created' => 'Sortieren nach Erstellungsdatum', 'books_sort_created' => 'Sortieren nach Erstellungsdatum',
@@ -331,9 +331,9 @@ return [
// Editor Sidebar // Editor Sidebar
'toggle_sidebar' => 'Seitenleiste umschalten', 'toggle_sidebar' => 'Seitenleiste umschalten',
'page_contents' => 'Seiteninhalt', 'page_contents' => 'Page Contents',
'page_contents_none' => 'Es wurden keine Überschriften im Seiteninhalt gefunden.', 'page_contents_none' => 'No headings were found in the page content.',
'page_contents_info' => 'Das Inhaltsmenü wird aus allen auf der Seite verwendeten Überschriften generiert.', 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
'page_tags' => 'Seiten-Schlagwörter', 'page_tags' => 'Seiten-Schlagwörter',
'chapter_tags' => 'Kapitel-Schlagwörter', 'chapter_tags' => 'Kapitel-Schlagwörter',
'book_tags' => 'Buch-Schlagwörter', 'book_tags' => 'Buch-Schlagwörter',

View File

@@ -207,7 +207,7 @@ return [
'role_all' => 'Alle', 'role_all' => 'Alle',
'role_own' => 'Eigene', 'role_own' => 'Eigene',
'role_controlled_by_asset' => 'Abhängig von dem Asset, in das sie hochgeladen werden', 'role_controlled_by_asset' => 'Abhängig von dem Asset, in das sie hochgeladen werden',
'role_controlled_by_page_delete' => 'Kontrolliert durch die Berechtigung zum Löschen einer Seite', 'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
'role_save' => 'Rolle speichern', 'role_save' => 'Rolle speichern',
'role_users' => 'Dieser Rolle zugeordnete Benutzer', 'role_users' => 'Dieser Rolle zugeordnete Benutzer',
'role_users_none' => 'Derzeit sind diesem Rollentyp keine Benutzer zugewiesen', '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_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',
'users_mfa_configure' => 'Methoden konfigurieren', 'users_mfa_configure' => 'Methoden konfigurieren',
'users_mfa_reset' => 'Setze Multifaktor-Authentifizierung zurück', '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, wird der Nutzer 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, 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_confirm' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchten?',
// API Tokens // API Tokens
@@ -366,7 +366,6 @@ return [
'ru' => 'Russisch', 'ru' => 'Russisch',
'sk' => 'Slowenisch', 'sk' => 'Slowenisch',
'sl' => 'Slowenisch', 'sl' => 'Slowenisch',
'sr' => 'Српски',
'sv' => 'Schwedisch', 'sv' => 'Schwedisch',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkisch', 'tr' => 'Türkisch',

View File

@@ -8,7 +8,7 @@ return [
'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.', 'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.',
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.',
'mfa_throttle' => 'Zu viele Multi-Faktor-Verifizierungsversuche. Bitte versuche es in :seconds Sekunden erneut.', 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.',
// Login & Register // Login & Register
'sign_up' => 'Registrieren', 'sign_up' => 'Registrieren',

View File

@@ -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_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' => 'Auto-Sortieroption',
'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName', 'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName',
'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_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_named' => 'Buch ":bookName" sortieren', 'books_sort_named' => 'Buch ":bookName" sortieren',
'books_sort_name' => 'Sortieren nach Namen', 'books_sort_name' => 'Sortieren nach Namen',
'books_sort_created' => 'Sortieren nach Erstellungsdatum', 'books_sort_created' => 'Sortieren nach Erstellungsdatum',
@@ -331,9 +331,9 @@ return [
// Editor Sidebar // Editor Sidebar
'toggle_sidebar' => 'Seitenleiste umschalten', 'toggle_sidebar' => 'Seitenleiste umschalten',
'page_contents' => 'Seiteninhalt', 'page_contents' => 'Page Contents',
'page_contents_none' => 'Es wurden keine Überschriften im Seiteninhalt gefunden.', 'page_contents_none' => 'No headings were found in the page content.',
'page_contents_info' => 'Das Inhaltsmenü wird aus allen auf der Seite verwendeten Überschriften generiert.', 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.',
'page_tags' => 'Seiten-Schlagwörter', 'page_tags' => 'Seiten-Schlagwörter',
'chapter_tags' => 'Kapitel-Schlagwörter', 'chapter_tags' => 'Kapitel-Schlagwörter',
'book_tags' => 'Buch-Schlagwörter', 'book_tags' => 'Buch-Schlagwörter',

View File

@@ -208,7 +208,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
'role_all' => 'Alle', 'role_all' => 'Alle',
'role_own' => 'Eigene', 'role_own' => 'Eigene',
'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt', 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',
'role_controlled_by_page_delete' => 'Kontrolliert durch die Berechtigung zum Löschen einer Seite', 'role_controlled_by_page_delete' => 'Controlled by page delete permissions',
'role_save' => 'Rolle speichern', 'role_save' => 'Rolle speichern',
'role_users' => 'Dieser Rolle zugeordnete Benutzer', 'role_users' => 'Dieser Rolle zugeordnete Benutzer',
'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet', '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_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert',
'users_mfa_configure' => 'Methoden konfigurieren', 'users_mfa_configure' => 'Methoden konfigurieren',
'users_mfa_reset' => 'Setze Multifaktor-Authentifizierung zurück', '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, wird der Nutzer 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, werden sie 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?', 'users_mfa_reset_confirm' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchten?',
// API Tokens // API Tokens
'user_api_token_create' => 'Neuen API-Token erstellen', 'user_api_token_create' => 'Neuen API-Token erstellen',
@@ -367,7 +367,6 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
'ru' => 'Russisch', 'ru' => 'Russisch',
'sk' => 'Slowenisch', 'sk' => 'Slowenisch',
'sl' => 'Slowenisch', 'sl' => 'Slowenisch',
'sr' => 'Српски',
'sv' => 'Schwedisch', 'sv' => 'Schwedisch',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkisch', 'tr' => 'Türkisch',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Ruso', 'ru' => 'Ruso',
'sk' => 'Eslovaco', 'sk' => 'Eslovaco',
'sl' => 'Esloveno', 'sl' => 'Esloveno',
'sr' => 'Српски',
'sv' => 'Sueco', 'sv' => 'Sueco',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Turco', 'tr' => 'Turco',

View File

@@ -367,7 +367,6 @@ return [
'ru' => 'Ruso', 'ru' => 'Ruso',
'sk' => 'Eslovaco', 'sk' => 'Eslovaco',
'sl' => 'Esloveno', 'sl' => 'Esloveno',
'sr' => 'Српски',
'sv' => 'Sueco', 'sv' => 'Sueco',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Turco', 'tr' => 'Turco',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский (vene keel)', 'ru' => 'Русский (vene keel)',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Sloveenia', 'sl' => 'Sloveenia',
'sr' => 'Српски',
'sv' => 'Rootsi', 'sv' => 'Rootsi',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türgi', 'tr' => 'Türgi',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Russe', 'ru' => 'Russe',
'sk' => 'Slovaque', 'sk' => 'Slovaque',
'sl' => 'Slovène', 'sl' => 'Slovène',
'sr' => 'Српски',
'sv' => 'Suédois', 'sv' => 'Suédois',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Turc', 'tr' => 'Turc',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,9 +366,8 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'Thai', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',
'uk' => 'Українська', 'uk' => 'Українська',
'uz' => 'Ozbekcha', 'uz' => 'Ozbekcha',

View File

@@ -99,7 +99,7 @@ return [
'user_update_notification' => 'Pengguna berhasil diperbarui', 'user_update_notification' => 'Pengguna berhasil diperbarui',
'user_delete' => 'pengguna yang dihapus', 'user_delete' => 'pengguna yang dihapus',
'user_delete_notification' => 'Pengguna berhasil dihapus', 'user_delete_notification' => 'Pengguna berhasil dihapus',
'user_mfa_reset' => 'atur ulang MFA untuk pengguna', 'user_mfa_reset' => 'reset MFA for user',
'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset',
// API Tokens // API Tokens

View File

@@ -8,8 +8,8 @@
return [ return [
// General editor terms // General editor terms
'general' => 'Umum', 'general' => 'Umum',
'advanced' => 'Tingkat lanjut', 'advanced' => 'Lanjutan',
'none' => 'Tidak Satupun', 'none' => 'Tidak Ada',
'cancel' => 'Batal', 'cancel' => 'Batal',
'save' => 'Simpan', 'save' => 'Simpan',
'close' => 'Tutup', 'close' => 'Tutup',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,7 +366,6 @@ return [
'ru' => 'Русский', 'ru' => 'Русский',
'sk' => 'Slovensky', 'sk' => 'Slovensky',
'sl' => 'Slovenščina', 'sl' => 'Slovenščina',
'sr' => 'Српски',
'sv' => 'Svenska', 'sv' => 'Svenska',
'th' => 'ภาษาไทย', 'th' => 'ภาษาไทย',
'tr' => 'Türkçe', 'tr' => 'Türkçe',

View File

@@ -366,9 +366,8 @@ return [
'ru' => 'Russo', 'ru' => 'Russo',
'sk' => 'Sloveno', 'sk' => 'Sloveno',
'sl' => 'Sloveno', 'sl' => 'Sloveno',
'sr' => 'Српски',
'sv' => 'Svedese', 'sv' => 'Svedese',
'th' => 'Thailandese', 'th' => 'ภาษาไทย',
'tr' => 'Turco', 'tr' => 'Turco',
'uk' => 'Ucraino', 'uk' => 'Ucraino',
'uz' => 'Ozbekcha', 'uz' => 'Ozbekcha',

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