0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-05-02 07:20:05 +00:00

Merge branch 'development' into release

This commit is contained in:
Dan Brown 2025-02-26 14:30:17 +00:00
commit b491b5fbca
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
388 changed files with 11326 additions and 4553 deletions
.github
LICENSE
app
composer.jsoncomposer.lock
database
dev
eslint.config.mjs
lang

View file

@ -461,3 +461,11 @@ Yannis Karlaftis (meliseus) :: Greek
felixxx :: German Informal felixxx :: German Informal
randi (randi65535) :: Korean randi (randi65535) :: Korean
test65428 :: Greek test65428 :: Greek
zeronell :: Chinese Simplified
julien Vinber (julienVinber) :: French
Hyunwoo Park (oksure) :: Korean
aram.rafeq.7 (aramrafeq2) :: Kurdish
Raphael Moreno (RaphaelMoreno) :: Portuguese, Brazilian
yn (user99) :: Arabic
Pavel Zlatarov (pzlatarov) :: Bulgarian
ingelres :: French

View file

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
php: ['8.1', '8.2', '8.3', '8.4'] php: ['8.2', '8.3', '8.4']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -13,10 +13,10 @@ on:
jobs: jobs:
build: build:
if: ${{ github.ref != 'refs/heads/l10n_development' }} if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
strategy: strategy:
matrix: matrix:
php: ['8.1', '8.2', '8.3', '8.4'] php: ['8.2', '8.3', '8.4']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015-2024, Dan Brown and the BookStack Project contributors. Copyright (c) 2015-2025, Dan Brown and the BookStack project contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -8,27 +8,15 @@ use Illuminate\Database\Eloquent\Model;
class ExternalBaseUserProvider implements UserProvider class ExternalBaseUserProvider implements UserProvider
{ {
/** public function __construct(
* The user model. protected string $model
* ) {
* @var string
*/
protected $model;
/**
* LdapUserProvider constructor.
*/
public function __construct(string $model)
{
$this->model = $model;
} }
/** /**
* Create a new instance of the model. * Create a new instance of the model.
*
* @return Model
*/ */
public function createModel() public function createModel(): Model
{ {
$class = '\\' . ltrim($this->model, '\\'); $class = '\\' . ltrim($this->model, '\\');
@ -37,12 +25,8 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Retrieve a user by their unique identifier. * Retrieve a user by their unique identifier.
*
* @param mixed $identifier
*
* @return Authenticatable|null
*/ */
public function retrieveById($identifier) public function retrieveById(mixed $identifier): ?Authenticatable
{ {
return $this->createModel()->newQuery()->find($identifier); return $this->createModel()->newQuery()->find($identifier);
} }
@ -50,12 +34,9 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Retrieve a user by their unique identifier and "remember me" token. * Retrieve a user by their unique identifier and "remember me" token.
* *
* @param mixed $identifier
* @param string $token * @param string $token
*
* @return Authenticatable|null
*/ */
public function retrieveByToken($identifier, $token) public function retrieveByToken(mixed $identifier, $token): null
{ {
return null; return null;
} }
@ -75,12 +56,8 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Retrieve a user by the given credentials. * Retrieve a user by the given credentials.
*
* @param array $credentials
*
* @return Authenticatable|null
*/ */
public function retrieveByCredentials(array $credentials) public function retrieveByCredentials(array $credentials): ?Authenticatable
{ {
// Search current user base by looking up a uid // Search current user base by looking up a uid
$model = $this->createModel(); $model = $this->createModel();
@ -92,15 +69,15 @@ class ExternalBaseUserProvider implements UserProvider
/** /**
* Validate a user against the given credentials. * Validate a user against the given credentials.
*
* @param Authenticatable $user
* @param array $credentials
*
* @return bool
*/ */
public function validateCredentials(Authenticatable $user, array $credentials) public function validateCredentials(Authenticatable $user, array $credentials): bool
{ {
// Should be done in the guard. // Should be done in the guard.
return false; return false;
} }
public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false)
{
// No action to perform, any passwords are external in the auth system
}
} }

View file

@ -54,7 +54,7 @@ class Ldap
* *
* @return \LDAP\Result|array|false * @return \LDAP\Result|array|false
*/ */
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null) public function search($ldapConnection, string $baseDn, string $filter, array $attributes = [])
{ {
return ldap_search($ldapConnection, $baseDn, $filter, $attributes); return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
} }
@ -66,7 +66,7 @@ class Ldap
* *
* @return \LDAP\Result|array|false * @return \LDAP\Result|array|false
*/ */
public function read($ldapConnection, string $baseDn, string $filter, array $attributes = null) public function read($ldapConnection, string $baseDn, string $filter, array $attributes = [])
{ {
return ldap_read($ldapConnection, $baseDn, $filter, $attributes); return ldap_read($ldapConnection, $baseDn, $filter, $attributes);
} }
@ -87,7 +87,7 @@ class Ldap
* *
* @param resource|\LDAP\Connection $ldapConnection * @param resource|\LDAP\Connection $ldapConnection
*/ */
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = null): array|false public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = []): array|false
{ {
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes); $search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
@ -99,7 +99,7 @@ class Ldap
* *
* @param resource|\LDAP\Connection $ldapConnection * @param resource|\LDAP\Connection $ldapConnection
*/ */
public function bind($ldapConnection, string $bindRdn = null, string $bindPassword = null): bool public function bind($ldapConnection, ?string $bindRdn = null, ?string $bindPassword = null): bool
{ {
return ldap_bind($ldapConnection, $bindRdn, $bindPassword); return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
} }

View file

@ -112,10 +112,14 @@ class LdapService
return null; return null;
} }
$userCn = $this->getUserResponseProperty($user, 'cn', null); $nameDefault = $this->getUserResponseProperty($user, 'cn', null);
if (is_null($nameDefault)) {
$nameDefault = ldap_explode_dn($user['dn'], 1)[0] ?? $user['dn'];
}
$formatted = [ $formatted = [
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']), 'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
'name' => $this->getUserDisplayName($user, $displayNameAttrs, $userCn), 'name' => $this->getUserDisplayName($user, $displayNameAttrs, $nameDefault),
'dn' => $user['dn'], 'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null), 'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null, 'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,

View file

@ -92,7 +92,7 @@ class SocialDriverManager
string $driverName, string $driverName,
array $config, array $config,
string $socialiteHandler, string $socialiteHandler,
callable $configureForRedirect = null ?callable $configureForRedirect = null
) { ) {
$this->validDrivers[] = $driverName; $this->validDrivers[] = $driverName;
config()->set('services.' . $driverName, $config); config()->set('services.' . $driverName, $config);

View file

@ -71,6 +71,10 @@ class ActivityType
const IMPORT_RUN = 'import_run'; const IMPORT_RUN = 'import_run';
const IMPORT_DELETE = 'import_delete'; const IMPORT_DELETE = 'import_delete';
const SORT_RULE_CREATE = 'sort_rule_create';
const SORT_RULE_UPDATE = 'sort_rule_update';
const SORT_RULE_DELETE = 'sort_rule_delete';
/** /**
* Get all the possible values. * Get all the possible values.
*/ */

View file

@ -5,6 +5,7 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Activity; use BookStack\Activity\Models\Activity;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use BookStack\Sorting\SortUrl;
use BookStack\Util\SimpleListOptions; use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -65,6 +66,7 @@ class AuditLogController extends Controller
'filters' => $filters, 'filters' => $filters,
'listOptions' => $listOptions, 'listOptions' => $listOptions,
'activityTypes' => $types, 'activityTypes' => $types,
'filterSortUrl' => new SortUrl('settings/audit', array_filter($request->except('page')))
]); ]);
} }
} }

View file

@ -26,7 +26,6 @@ class Comment extends Model implements Loggable
use HasCreatorAndUpdater; use HasCreatorAndUpdater;
protected $fillable = ['parent_id']; protected $fillable = ['parent_id'];
protected $appends = ['created', 'updated'];
/** /**
* Get the entity that this comment belongs to. * Get the entity that this comment belongs to.
@ -54,22 +53,6 @@ class Comment extends Model implements Loggable
return $this->updated_at->timestamp > $this->created_at->timestamp; return $this->updated_at->timestamp > $this->created_at->timestamp;
} }
/**
* Get created date as a relative diff.
*/
public function getCreatedAttribute(): string
{
return $this->created_at->diffForHumans();
}
/**
* Get updated date as a relative diff.
*/
public function getUpdatedAttribute(): string
{
return $this->updated_at->diffForHumans();
}
public function logDescriptor(): string public function logDescriptor(): string
{ {
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})"; return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";

View file

@ -42,4 +42,12 @@ class EventServiceProvider extends ServiceProvider
{ {
return false; return false;
} }
/**
* Overrides the registration of Laravel's default email verification system
*/
protected function configureEmailVerification(): void
{
//
}
} }

View file

@ -1,6 +1,7 @@
<?php <?php
use BookStack\App\Model; use BookStack\App\Model;
use BookStack\Facades\Theme;
use BookStack\Permissions\PermissionApplicator; use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
use BookStack\Users\Models\User; use BookStack\Users\Models\User;
@ -42,9 +43,9 @@ 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, Model $ownable = null): bool function userCan(string $permission, ?Model $ownable = null): bool
{ {
if ($ownable === null) { if (is_null($ownable)) {
return user()->can($permission); return user()->can($permission);
} }
@ -70,7 +71,7 @@ function userCanOnAny(string $action, string $entityClass = ''): bool
* *
* @return mixed|SettingService * @return mixed|SettingService
*/ */
function setting(string $key = null, $default = null) function setting(?string $key = null, mixed $default = null): mixed
{ {
$settingService = app()->make(SettingService::class); $settingService = app()->make(SettingService::class);
@ -88,43 +89,10 @@ function setting(string $key = null, $default = null)
*/ */
function theme_path(string $path = ''): ?string function theme_path(string $path = ''): ?string
{ {
$theme = config('view.theme'); $theme = Theme::getTheme();
if (!$theme) { if (!$theme) {
return null; return null;
} }
return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path)); return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path));
} }
/**
* Generate a URL with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
*/
function sortUrl(string $path, array $data, array $overrideData = []): string
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
// Change sorting direction is already sorted on current attribute
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
} elseif (isset($overrideData['sort'])) {
$queryData['order'] = 'asc';
}
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') {
continue;
}
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
if (count($queryStringSections) === 0) {
return url($path);
}
return url($path . '?' . implode('&', $queryStringSections));
}

View file

@ -1,37 +0,0 @@
<?php
/**
* Broadcasting configuration options.
*
* Changes to these config files are not supported by BookStack and may break upon updates.
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
return [
// Default Broadcaster
// This option controls the default broadcaster that will be used by the
// framework when an event needs to be broadcast. This can be set to
// any of the connections defined in the "connections" array below.
'default' => 'null',
// Broadcast Connections
// Here you may define all of the broadcast connections that will be used
// to broadcast events to other systems or over websockets. Samples of
// each available type of connection are provided inside this array.
'connections' => [
// Default options removed since we don't use broadcasting.
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View file

@ -35,10 +35,6 @@ return [
// Available caches stores // Available caches stores
'stores' => [ 'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [ 'array' => [
'driver' => 'array', 'driver' => 'array',
'serialize' => false, 'serialize' => false,
@ -49,6 +45,7 @@ return [
'table' => 'cache', 'table' => 'cache',
'connection' => null, 'connection' => null,
'lock_connection' => null, 'lock_connection' => null,
'lock_table' => null,
], ],
'file' => [ 'file' => [

View file

@ -114,6 +114,7 @@ return [
* @var array * @var array
*/ */
'allowed_protocols' => [ 'allowed_protocols' => [
"data://" => ["rules" => []],
'file://' => ['rules' => []], 'file://' => ['rules' => []],
'http://' => ['rules' => []], 'http://' => ['rules' => []],
'https://' => ['rules' => []], 'https://' => ['rules' => []],

View file

@ -33,12 +33,14 @@ return [
'driver' => 'local', 'driver' => 'local',
'root' => public_path(), 'root' => public_path(),
'visibility' => 'public', 'visibility' => 'public',
'serve' => false,
'throw' => true, 'throw' => true,
], ],
'local_secure_attachments' => [ 'local_secure_attachments' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('uploads/files/'), 'root' => storage_path('uploads/files/'),
'serve' => false,
'throw' => true, 'throw' => true,
], ],
@ -46,6 +48,7 @@ return [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('uploads/images/'), 'root' => storage_path('uploads/images/'),
'visibility' => 'public', 'visibility' => 'public',
'serve' => false,
'throw' => true, 'throw' => true,
], ],

View file

@ -38,7 +38,7 @@ return [
'password' => env('MAIL_PASSWORD'), 'password' => env('MAIL_PASSWORD'),
'verify_peer' => env('MAIL_VERIFY_SSL', true), 'verify_peer' => env('MAIL_VERIFY_SSL', true),
'timeout' => null, 'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'), 'local_domain' => null,
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'), 'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
], ],
@ -64,12 +64,4 @@ return [
], ],
], ],
], ],
// Email markdown configuration
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
]; ];

View file

@ -23,6 +23,7 @@ return [
'database' => [ 'database' => [
'driver' => 'database', 'driver' => 'database',
'connection' => null,
'table' => 'jobs', 'table' => 'jobs',
'queue' => 'default', 'queue' => 'default',
'retry_after' => 90, 'retry_after' => 90,

View file

@ -0,0 +1,99 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Entities\Models\Book;
use BookStack\Sorting\BookSorter;
use BookStack\Sorting\SortRule;
use Illuminate\Console\Command;
class AssignSortRuleCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:assign-sort-rule
{sort-rule=0: ID of the sort rule to apply}
{--all-books : Apply to all books in the system}
{--books-without-sort : Apply to only books without a sort rule already assigned}
{--books-with-sort= : Apply to only books with the sort rule of given id}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Assign a sort rule to content in the system';
/**
* Execute the console command.
*/
public function handle(BookSorter $sorter): int
{
$sortRuleId = intval($this->argument('sort-rule')) ?? 0;
if ($sortRuleId === 0) {
return $this->listSortRules();
}
$rule = SortRule::query()->find($sortRuleId);
if ($this->option('all-books')) {
$query = Book::query();
} else if ($this->option('books-without-sort')) {
$query = Book::query()->whereNull('sort_rule_id');
} else if ($this->option('books-with-sort')) {
$sortId = intval($this->option('books-with-sort')) ?: 0;
if (!$sortId) {
$this->error("Provided --books-with-sort option value is invalid");
return 1;
}
$query = Book::query()->where('sort_rule_id', $sortId);
} else {
$this->error("No option provided to specify target. Run with the -h option to see all available options.");
return 1;
}
if (!$rule) {
$this->error("Sort rule of provided id {$sortRuleId} not found!");
return 1;
}
$count = $query->clone()->count();
$this->warn("This will apply sort rule [{$rule->id}: {$rule->name}] to {$count} book(s) and run the sort on each.");
$confirmed = $this->confirm("Are you sure you want to continue?");
if (!$confirmed) {
return 1;
}
$processed = 0;
$query->chunkById(10, function ($books) use ($rule, $sorter, $count, &$processed) {
$max = min($count, ($processed + 10));
$this->info("Applying to {$processed}-{$max} of {$count} books");
foreach ($books as $book) {
$book->sort_rule_id = $rule->id;
$book->save();
$sorter->runBookAutoSort($book);
}
$processed = $max;
});
$this->info("Sort applied to {$processed} book(s)!");
return 0;
}
protected function listSortRules(): int
{
$rules = SortRule::query()->orderBy('id', 'asc')->get();
$this->error("Sort rule ID required!");
$this->warn("\nAvailable sort rules:");
foreach ($rules as $rule) {
$this->info("{$rule->id}: {$rule->name}");
}
return 1;
}
}

View file

@ -70,7 +70,7 @@ class BookController extends Controller
/** /**
* Show the form for creating a new book. * Show the form for creating a new book.
*/ */
public function create(string $shelfSlug = null) public function create(?string $shelfSlug = null)
{ {
$this->checkPermission('book-create-all'); $this->checkPermission('book-create-all');
@ -93,7 +93,7 @@ class BookController extends Controller
* @throws ImageUploadException * @throws ImageUploadException
* @throws ValidationException * @throws ValidationException
*/ */
public function store(Request $request, string $shelfSlug = null) public function store(Request $request, ?string $shelfSlug = null)
{ {
$this->checkPermission('book-create-all'); $this->checkPermission('book-create-all');
$validated = $this->validate($request, [ $validated = $this->validate($request, [

View file

@ -41,7 +41,7 @@ class PageController extends Controller
* *
* @throws Throwable * @throws Throwable
*/ */
public function create(string $bookSlug, string $chapterSlug = null) public function create(string $bookSlug, ?string $chapterSlug = null)
{ {
if ($chapterSlug) { if ($chapterSlug) {
$parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
@ -69,7 +69,7 @@ class PageController extends Controller
* *
* @throws ValidationException * @throws ValidationException
*/ */
public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null) public function createAsGuest(Request $request, string $bookSlug, ?string $chapterSlug = null)
{ {
$this->validate($request, [ $this->validate($request, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],

View file

@ -2,6 +2,7 @@
namespace BookStack\Entities\Models; namespace BookStack\Entities\Models;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\Image; use BookStack\Uploads\Image;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -16,12 +17,14 @@ use Illuminate\Support\Collection;
* @property string $description * @property string $description
* @property int $image_id * @property int $image_id
* @property ?int $default_template_id * @property ?int $default_template_id
* @property ?int $sort_rule_id
* @property Image|null $cover * @property Image|null $cover
* @property \Illuminate\Database\Eloquent\Collection $chapters * @property \Illuminate\Database\Eloquent\Collection $chapters
* @property \Illuminate\Database\Eloquent\Collection $pages * @property \Illuminate\Database\Eloquent\Collection $pages
* @property \Illuminate\Database\Eloquent\Collection $directPages * @property \Illuminate\Database\Eloquent\Collection $directPages
* @property \Illuminate\Database\Eloquent\Collection $shelves * @property \Illuminate\Database\Eloquent\Collection $shelves
* @property ?Page $defaultTemplate * @property ?Page $defaultTemplate
* @property ?SortRule $sortRule
*/ */
class Book extends Entity implements HasCoverImage class Book extends Entity implements HasCoverImage
{ {
@ -82,6 +85,14 @@ class Book extends Entity implements HasCoverImage
return $this->belongsTo(Page::class, 'default_template_id'); return $this->belongsTo(Page::class, 'default_template_id');
} }
/**
* Get the sort set assigned to this book, if existing.
*/
public function sortRule(): BelongsTo
{
return $this->belongsTo(SortRule::class);
}
/** /**
* Get all pages within this book. * Get all pages within this book.
*/ */

View file

@ -18,7 +18,7 @@ class QueryPopular
) { ) {
} }
public function run(int $count, int $page, array $filterModels = null): Collection public function run(int $count, int $page, array $filterModels): Collection
{ {
$query = $this->permissions $query = $this->permissions
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type') ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
@ -26,7 +26,7 @@ class QueryPopular
->groupBy('viewable_id', 'viewable_type') ->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc'); ->orderBy('view_count', 'desc');
if ($filterModels) { if (!empty($filterModels)) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels)); $query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
} }

View file

@ -4,6 +4,7 @@ namespace BookStack\Entities\Repos;
use BookStack\Activity\TagRepo; use BookStack\Activity\TagRepo;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\HasCoverImage;
@ -12,6 +13,7 @@ use BookStack\Entities\Queries\PageQueries;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\References\ReferenceStore; use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater; use BookStack\References\ReferenceUpdater;
use BookStack\Sorting\BookSorter;
use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageRepo;
use BookStack\Util\HtmlDescriptionFilter; use BookStack\Util\HtmlDescriptionFilter;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
@ -24,6 +26,7 @@ class BaseRepo
protected ReferenceUpdater $referenceUpdater, protected ReferenceUpdater $referenceUpdater,
protected ReferenceStore $referenceStore, protected ReferenceStore $referenceStore,
protected PageQueries $pageQueries, protected PageQueries $pageQueries,
protected BookSorter $bookSorter,
) { ) {
} }
@ -134,6 +137,18 @@ class BaseRepo
$entity->save(); $entity->save();
} }
/**
* Sort the parent of the given entity, if any auto sort actions are set for it.
* Typical ran during create/update/insert events.
*/
public function sortParent(Entity $entity): void
{
if ($entity instanceof BookChild) {
$book = $entity->book;
$this->bookSorter->runBookAutoSort($book);
}
}
protected function updateDescription(Entity $entity, array $input): void protected function updateDescription(Entity $entity, array $input): void
{ {
if (!in_array(HasHtmlDescription::class, class_uses($entity))) { if (!in_array(HasHtmlDescription::class, class_uses($entity))) {

View file

@ -8,6 +8,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\TrashCan; use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageRepo;
use Exception; use Exception;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
@ -33,6 +34,12 @@ class BookRepo
$this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null)); $this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::BOOK_CREATE, $book); Activity::add(ActivityType::BOOK_CREATE, $book);
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
$book->sort_rule_id = $defaultBookSortSetting;
$book->save();
}
return $book; return $book;
} }

View file

@ -34,6 +34,8 @@ class ChapterRepo
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null)); $this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::CHAPTER_CREATE, $chapter); Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
$this->baseRepo->sortParent($chapter);
return $chapter; return $chapter;
} }
@ -50,6 +52,8 @@ class ChapterRepo
Activity::add(ActivityType::CHAPTER_UPDATE, $chapter); Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);
$this->baseRepo->sortParent($chapter);
return $chapter; return $chapter;
} }
@ -88,6 +92,8 @@ class ChapterRepo
$chapter->rebuildPermissions(); $chapter->rebuildPermissions();
Activity::add(ActivityType::CHAPTER_MOVE, $chapter); Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
$this->baseRepo->sortParent($chapter);
return $parent; return $parent;
} }
} }

View file

@ -83,6 +83,7 @@ class PageRepo
$draft->refresh(); $draft->refresh();
Activity::add(ActivityType::PAGE_CREATE, $draft); Activity::add(ActivityType::PAGE_CREATE, $draft);
$this->baseRepo->sortParent($draft);
return $draft; return $draft;
} }
@ -128,6 +129,7 @@ class PageRepo
} }
Activity::add(ActivityType::PAGE_UPDATE, $page); Activity::add(ActivityType::PAGE_UPDATE, $page);
$this->baseRepo->sortParent($page);
return $page; return $page;
} }
@ -243,6 +245,8 @@ class PageRepo
Activity::add(ActivityType::PAGE_RESTORE, $page); Activity::add(ActivityType::PAGE_RESTORE, $page);
Activity::add(ActivityType::REVISION_RESTORE, $revision); Activity::add(ActivityType::REVISION_RESTORE, $revision);
$this->baseRepo->sortParent($page);
return $page; return $page;
} }
@ -272,6 +276,8 @@ class PageRepo
Activity::add(ActivityType::PAGE_MOVE, $page); Activity::add(ActivityType::PAGE_MOVE, $page);
$this->baseRepo->sortParent($page);
return $parent; return $parent;
} }

View file

@ -46,7 +46,7 @@ class RevisionRepo
/** /**
* Store a new revision in the system for the given page. * Store a new revision in the system for the given page.
*/ */
public function storeNewForPage(Page $page, string $summary = null): PageRevision public function storeNewForPage(Page $page, ?string $summary = null): PageRevision
{ {
$revision = new PageRevision(); $revision = new PageRevision();

View file

@ -8,6 +8,8 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\EntityQueries;
use BookStack\Sorting\BookSortMap;
use BookStack\Sorting\BookSortMapItem;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class BookContents class BookContents
@ -103,211 +105,4 @@ class BookContents
return $query->where('book_id', '=', $this->book->id)->get(); return $query->where('book_id', '=', $this->book->id)->get();
} }
/**
* Sort the books content using the given sort map.
* Returns a list of books that were involved in the operation.
*
* @returns Book[]
*/
public function sortUsingMap(BookSortMap $sortMap): array
{
// Load models into map
$modelMap = $this->loadModelsFromSortMap($sortMap);
// Sort our changes from our map to be chapters first
// Since they need to be process to ensure book alignment for child page changes.
$sortMapItems = $sortMap->all();
usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) {
$aScore = $itemA->type === 'page' ? 2 : 1;
$bScore = $itemB->type === 'page' ? 2 : 1;
return $aScore - $bScore;
});
// Perform the sort
foreach ($sortMapItems as $item) {
$this->applySortUpdates($item, $modelMap);
}
/** @var Book[] $booksInvolved */
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
return str_starts_with($key, 'book:');
}, ARRAY_FILTER_USE_KEY));
// Update permissions of books involved
foreach ($booksInvolved as $book) {
$book->rebuildPermissions();
}
return $booksInvolved;
}
/**
* Using the given sort map item, detect changes for the related model
* and update it if required. Changes where permissions are lacking will
* be skipped and not throw an error.
*
* @param array<string, Entity> $modelMap
*/
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
{
/** @var BookChild $model */
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
if (!$model) {
return;
}
$priorityChanged = $model->priority !== $sortMapItem->sort;
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
// Stop if there's no change
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
return;
}
$currentParentKey = 'book:' . $model->book_id;
if ($model instanceof Page && $model->chapter_id) {
$currentParentKey = 'chapter:' . $model->chapter_id;
}
$currentParent = $modelMap[$currentParentKey] ?? null;
/** @var Book $newBook */
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
/** @var ?Chapter $newChapter */
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
return;
}
// Action the required changes
if ($bookChanged) {
$model->changeBook($newBook->id);
}
if ($model instanceof Page && $chapterChanged) {
$model->chapter_id = $newChapter->id ?? 0;
}
if ($priorityChanged) {
$model->priority = $sortMapItem->sort;
}
if ($chapterChanged || $priorityChanged) {
$model->save();
}
}
/**
* Check if the current user has permissions to apply the given sorting change.
* Is quite complex since items can gain a different parent change. Acts as a:
* - Update of old parent element (Change of content/order).
* - Update of sorted/moved element.
* - Deletion of element (Relative to parent upon move).
* - Creation of element within parent (Upon move to new parent).
*/
protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
{
// Stop if we can't see the current parent or new book.
if (!$currentParent || !$newBook) {
return false;
}
$hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));
if ($model instanceof Chapter) {
$hasPermission = userCan('book-update', $currentParent)
&& userCan('book-update', $newBook)
&& userCan('chapter-update', $model)
&& (!$hasNewParent || userCan('chapter-create', $newBook))
&& (!$hasNewParent || userCan('chapter-delete', $model));
if (!$hasPermission) {
return false;
}
}
if ($model instanceof Page) {
$parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasCurrentParentPermission = userCan($parentPermission, $currentParent);
// This needs to check if there was an intended chapter location in the original sort map
// rather than inferring from the $newChapter since that variable may be null
// due to other reasons (Visibility).
$newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;
if (!$newParent) {
return false;
}
$hasPageEditPermission = userCan('page-update', $model);
$newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id));
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasNewParentPermission = userCan($newParentPermission, $newParent);
$hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model));
$hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent));
$hasPermission = $hasCurrentParentPermission
&& $newParentInRightLocation
&& $hasNewParentPermission
&& $hasPageEditPermission
&& $hasDeletePermissionIfMoving
&& $hasCreatePermissionIfMoving;
if (!$hasPermission) {
return false;
}
}
return true;
}
/**
* Load models from the database into the given sort map.
*
* @return array<string, Entity>
*/
protected function loadModelsFromSortMap(BookSortMap $sortMap): array
{
$modelMap = [];
$ids = [
'chapter' => [],
'page' => [],
'book' => [],
];
foreach ($sortMap->all() as $sortMapItem) {
$ids[$sortMapItem->type][] = $sortMapItem->id;
$ids['book'][] = $sortMapItem->parentBookId;
if ($sortMapItem->parentChapterId) {
$ids['chapter'][] = $sortMapItem->parentChapterId;
}
}
$pages = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();
/** @var Page $page */
foreach ($pages as $page) {
$modelMap['page:' . $page->id] = $page;
$ids['book'][] = $page->book_id;
if ($page->chapter_id) {
$ids['chapter'][] = $page->chapter_id;
}
}
$chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();
/** @var Chapter $chapter */
foreach ($chapters as $chapter) {
$modelMap['chapter:' . $chapter->id] = $chapter;
$ids['book'][] = $chapter->book_id;
}
$books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();
/** @var Book $book */
foreach ($books as $book) {
$modelMap['book:' . $book->id] = $book;
}
return $modelMap;
}
} }

View file

@ -76,6 +76,6 @@ class BookExportController extends Controller
$book = $this->queries->findVisibleBySlugOrFail($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$zip = $builder->buildForBook($book); $zip = $builder->buildForBook($book);
return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', filesize($zip), true); return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', true);
} }
} }

View file

@ -82,6 +82,6 @@ class ChapterExportController extends Controller
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$zip = $builder->buildForChapter($chapter); $zip = $builder->buildForChapter($chapter);
return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', filesize($zip), true); return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', true);
} }
} }

View file

@ -86,6 +86,6 @@ class PageExportController extends Controller
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$zip = $builder->buildForPage($page); $zip = $builder->buildForPage($page);
return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', filesize($zip), true); return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', true);
} }
} }

View file

@ -39,8 +39,9 @@ class DownloadResponseFactory
* Create a response that downloads the given file via a stream. * Create a response that downloads the given file via a stream.
* Has the option to delete the provided file once the stream is closed. * Has the option to delete the provided file once the stream is closed.
*/ */
public function streamedFileDirectly(string $filePath, string $fileName, int $fileSize, bool $deleteAfter = false): StreamedResponse public function streamedFileDirectly(string $filePath, string $fileName, bool $deleteAfter = false): StreamedResponse
{ {
$fileSize = filesize($filePath);
$stream = fopen($filePath, 'r'); $stream = fopen($filePath, 'r');
if ($deleteAfter) { if ($deleteAfter) {
@ -69,7 +70,7 @@ class DownloadResponseFactory
public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
{ {
$rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request); $rangeStream = new RangeSupportedStream($stream, $fileSize, $this->request);
$mime = $rangeStream->sniffMime(); $mime = $rangeStream->sniffMime(pathinfo($fileName, PATHINFO_EXTENSION));
$headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders()); $headers = array_merge($this->getHeaders($fileName, $fileSize, $mime), $rangeStream->getResponseHeaders());
return response()->stream( return response()->stream(
@ -79,6 +80,22 @@ class DownloadResponseFactory
); );
} }
/**
* Create a response that provides the given file via a stream with detected content-type.
* Has the option to delete the provided file once the stream is closed.
*/
public function streamedFileInline(string $filePath, ?string $fileName = null): StreamedResponse
{
$fileSize = filesize($filePath);
$stream = fopen($filePath, 'r');
if ($fileName === null) {
$fileName = basename($filePath);
}
return $this->streamedInline($stream, $fileName, $fileSize);
}
/** /**
* Get the common headers to provide for a download response. * Get the common headers to provide for a download response.
*/ */

View file

@ -7,6 +7,13 @@ use Symfony\Component\HttpFoundation\Response;
class PreventResponseCaching class PreventResponseCaching
{ {
/**
* Paths to ignore when preventing response caching.
*/
protected array $ignoredPathPrefixes = [
'theme/',
];
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
@ -20,6 +27,13 @@ class PreventResponseCaching
/** @var Response $response */ /** @var Response $response */
$response = $next($request); $response = $next($request);
$path = $request->path();
foreach ($this->ignoredPathPrefixes as $ignoredPath) {
if (str_starts_with($path, $ignoredPath)) {
return $response;
}
}
$response->headers->set('Cache-Control', 'no-cache, no-store, private'); $response->headers->set('Cache-Control', 'no-cache, no-store, private');
$response->headers->set('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT'); $response->headers->set('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');

View file

@ -32,12 +32,12 @@ class RangeSupportedStream
/** /**
* Sniff a mime type from the stream. * Sniff a mime type from the stream.
*/ */
public function sniffMime(): string public function sniffMime(string $extension = ''): string
{ {
$offset = min(2000, $this->fileSize); $offset = min(2000, $this->fileSize);
$this->sniffContent = fread($this->stream, $offset); $this->sniffContent = fread($this->stream, $offset);
return (new WebSafeMimeSniffer())->sniff($this->sniffContent); return (new WebSafeMimeSniffer())->sniff($this->sniffContent, $extension);
} }
/** /**

View file

@ -16,7 +16,13 @@ class SearchIndex
/** /**
* A list of delimiter characters used to break-up parsed content into terms for indexing. * A list of delimiter characters used to break-up parsed content into terms for indexing.
*/ */
public static string $delimiters = " \n\t.,!?:;()[]{}<>`'\""; public static string $delimiters = " \n\t.-,!?:;()[]{}<>`'\"«»";
/**
* A list of delimiter which could be commonly used within a single term and also indicate a break between terms.
* The indexer will index the full term with these delimiters, plus the terms split via these delimiters.
*/
public static string $softDelimiters = ".-";
public function __construct( public function __construct(
protected EntityProvider $entityProvider protected EntityProvider $entityProvider
@ -196,15 +202,36 @@ class SearchIndex
protected function textToTermCountMap(string $text): array protected function textToTermCountMap(string $text): array
{ {
$tokenMap = []; // {TextToken => OccurrenceCount} $tokenMap = []; // {TextToken => OccurrenceCount}
$splitChars = static::$delimiters; $softDelims = static::$softDelimiters;
$token = strtok($text, $splitChars); $tokenizer = new SearchTextTokenizer($text, static::$delimiters);
$extendedToken = '';
$extendedLen = 0;
$token = $tokenizer->next();
while ($token !== false) { while ($token !== false) {
if (!isset($tokenMap[$token])) { $delim = $tokenizer->previousDelimiter();
$tokenMap[$token] = 0;
if ($delim && str_contains($softDelims, $delim) && $token !== '') {
$extendedToken .= $delim . $token;
$extendedLen++;
} else {
if ($extendedLen > 1) {
$tokenMap[$extendedToken] = ($tokenMap[$extendedToken] ?? 0) + 1;
}
$extendedToken = $token;
$extendedLen = 1;
} }
$tokenMap[$token]++;
$token = strtok($splitChars); if ($token) {
$tokenMap[$token] = ($tokenMap[$token] ?? 0) + 1;
}
$token = $tokenizer->next();
}
if ($extendedLen > 1) {
$tokenMap[$extendedToken] = ($tokenMap[$extendedToken] ?? 0) + 1;
} }
return $tokenMap; return $tokenMap;

View file

@ -181,7 +181,7 @@ class SearchOptions
protected static function parseStandardTermString(string $termString): array protected static function parseStandardTermString(string $termString): array
{ {
$terms = explode(' ', $termString); $terms = explode(' ', $termString);
$indexDelimiters = SearchIndex::$delimiters; $indexDelimiters = implode('', array_diff(str_split(SearchIndex::$delimiters), str_split(SearchIndex::$softDelimiters)));
$parsed = [ $parsed = [
'terms' => [], 'terms' => [],
'exacts' => [], 'exacts' => [],

View file

@ -0,0 +1,70 @@
<?php
namespace BookStack\Search;
/**
* A custom text tokenizer which records & provides insight needed for our search indexing.
* We used to use basic strtok() but this class does the following which that lacked:
* - Tracks and provides the current/previous delimiter that we've stopped at.
* - Returns empty tokens upon parsing a delimiter.
*/
class SearchTextTokenizer
{
protected int $currentIndex = 0;
protected int $length;
protected string $currentDelimiter = '';
protected string $previousDelimiter = '';
public function __construct(
protected string $text,
protected string $delimiters = ' '
) {
$this->length = strlen($this->text);
}
/**
* Get the current delimiter to be found.
*/
public function currentDelimiter(): string
{
return $this->currentDelimiter;
}
/**
* Get the previous delimiter found.
*/
public function previousDelimiter(): string
{
return $this->previousDelimiter;
}
/**
* Get the next token between delimiters.
* Returns false if there's no further tokens.
*/
public function next(): string|false
{
$token = '';
for ($i = $this->currentIndex; $i < $this->length; $i++) {
$char = $this->text[$i];
if (str_contains($this->delimiters, $char)) {
$this->previousDelimiter = $this->currentDelimiter;
$this->currentDelimiter = $char;
$this->currentIndex = $i + 1;
return $token;
}
$token .= $char;
}
if ($token) {
$this->currentIndex = $this->length;
$this->previousDelimiter = $this->currentDelimiter;
$this->currentDelimiter = '';
return $token;
}
return false;
}
}

View file

@ -1,11 +1,10 @@
<?php <?php
namespace BookStack\Entities\Controllers; namespace BookStack\Sorting;
use BookStack\Activity\ActivityType; use BookStack\Activity\ActivityType;
use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\BookContents; use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Http\Controller; use BookStack\Http\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -45,25 +44,40 @@ class BookSortController extends Controller
} }
/** /**
* Sorts a book using a given mapping array. * Update the sort options of a book, setting the auto-sort and/or updating
* child order via mapping.
*/ */
public function update(Request $request, string $bookSlug) public function update(Request $request, BookSorter $sorter, string $bookSlug)
{ {
$book = $this->queries->findVisibleBySlugOrFail($bookSlug); $book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book); $this->checkOwnablePermission('book-update', $book);
$loggedActivityForBook = false;
// Return if no map sent // Sort via map
if (!$request->filled('sort-tree')) { if ($request->filled('sort-tree')) {
return redirect($book->getUrl()); $sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$booksInvolved = $sorter->sortUsingMap($sortMap);
// Rebuild permissions and add activity for involved books.
foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
if ($bookInvolved->id === $book->id) {
$loggedActivityForBook = true;
}
}
} }
$sortMap = BookSortMap::fromJson($request->get('sort-tree')); if ($request->filled('auto-sort')) {
$bookContents = new BookContents($book); $sortSetId = intval($request->get('auto-sort')) ?: null;
$booksInvolved = $bookContents->sortUsingMap($sortMap); if ($sortSetId && SortRule::query()->find($sortSetId) === null) {
$sortSetId = null;
// Rebuild permissions and add activity for involved books. }
foreach ($booksInvolved as $bookInvolved) { $book->sort_rule_id = $sortSetId;
Activity::add(ActivityType::BOOK_SORT, $bookInvolved); $book->save();
$sorter->runBookAutoSort($book);
if (!$loggedActivityForBook) {
Activity::add(ActivityType::BOOK_SORT, $book);
}
} }
return redirect($book->getUrl()); return redirect($book->getUrl());

View file

@ -1,6 +1,6 @@
<?php <?php
namespace BookStack\Entities\Tools; namespace BookStack\Sorting;
class BookSortMap class BookSortMap
{ {

View file

@ -1,6 +1,6 @@
<?php <?php
namespace BookStack\Entities\Tools; namespace BookStack\Sorting;
class BookSortMapItem class BookSortMapItem
{ {

284
app/Sorting/BookSorter.php Normal file
View file

@ -0,0 +1,284 @@
<?php
namespace BookStack\Sorting;
use BookStack\App\Model;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\EntityQueries;
class BookSorter
{
public function __construct(
protected EntityQueries $queries,
) {
}
public function runBookAutoSortForAllWithSet(SortRule $set): void
{
$set->books()->chunk(50, function ($books) {
foreach ($books as $book) {
$this->runBookAutoSort($book);
}
});
}
/**
* Runs the auto-sort for a book if the book has a sort set applied to it.
* This does not consider permissions since the sort operations are centrally
* managed by admins so considered permitted if existing and assigned.
*/
public function runBookAutoSort(Book $book): void
{
$set = $book->sortRule;
if (!$set) {
return;
}
$sortFunctions = array_map(function (SortRuleOperation $op) {
return $op->getSortFunction();
}, $set->getOperations());
$chapters = $book->chapters()
->with('pages:id,name,priority,created_at,updated_at,chapter_id')
->get(['id', 'name', 'priority', 'created_at', 'updated_at']);
/** @var (Chapter|Book)[] $topItems */
$topItems = [
...$book->directPages()->get(['id', 'name', 'priority', 'created_at', 'updated_at']),
...$chapters,
];
foreach ($sortFunctions as $sortFunction) {
usort($topItems, $sortFunction);
}
foreach ($topItems as $index => $topItem) {
$topItem->priority = $index + 1;
$topItem::withoutTimestamps(fn () => $topItem->save());
}
foreach ($chapters as $chapter) {
$pages = $chapter->pages->all();
foreach ($sortFunctions as $sortFunction) {
usort($pages, $sortFunction);
}
foreach ($pages as $index => $page) {
$page->priority = $index + 1;
$page::withoutTimestamps(fn () => $page->save());
}
}
}
/**
* Sort the books content using the given sort map.
* Returns a list of books that were involved in the operation.
*
* @returns Book[]
*/
public function sortUsingMap(BookSortMap $sortMap): array
{
// Load models into map
$modelMap = $this->loadModelsFromSortMap($sortMap);
// Sort our changes from our map to be chapters first
// Since they need to be process to ensure book alignment for child page changes.
$sortMapItems = $sortMap->all();
usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) {
$aScore = $itemA->type === 'page' ? 2 : 1;
$bScore = $itemB->type === 'page' ? 2 : 1;
return $aScore - $bScore;
});
// Perform the sort
foreach ($sortMapItems as $item) {
$this->applySortUpdates($item, $modelMap);
}
/** @var Book[] $booksInvolved */
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
return str_starts_with($key, 'book:');
}, ARRAY_FILTER_USE_KEY));
// Update permissions of books involved
foreach ($booksInvolved as $book) {
$book->rebuildPermissions();
}
return $booksInvolved;
}
/**
* Using the given sort map item, detect changes for the related model
* and update it if required. Changes where permissions are lacking will
* be skipped and not throw an error.
*
* @param array<string, Entity> $modelMap
*/
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
{
/** @var BookChild $model */
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
if (!$model) {
return;
}
$priorityChanged = $model->priority !== $sortMapItem->sort;
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
// Stop if there's no change
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
return;
}
$currentParentKey = 'book:' . $model->book_id;
if ($model instanceof Page && $model->chapter_id) {
$currentParentKey = 'chapter:' . $model->chapter_id;
}
$currentParent = $modelMap[$currentParentKey] ?? null;
/** @var Book $newBook */
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
/** @var ?Chapter $newChapter */
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
return;
}
// Action the required changes
if ($bookChanged) {
$model->changeBook($newBook->id);
}
if ($model instanceof Page && $chapterChanged) {
$model->chapter_id = $newChapter->id ?? 0;
}
if ($priorityChanged) {
$model->priority = $sortMapItem->sort;
}
if ($chapterChanged || $priorityChanged) {
$model::withoutTimestamps(fn () => $model->save());
}
}
/**
* Check if the current user has permissions to apply the given sorting change.
* Is quite complex since items can gain a different parent change. Acts as a:
* - Update of old parent element (Change of content/order).
* - Update of sorted/moved element.
* - Deletion of element (Relative to parent upon move).
* - Creation of element within parent (Upon move to new parent).
*/
protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
{
// Stop if we can't see the current parent or new book.
if (!$currentParent || !$newBook) {
return false;
}
$hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));
if ($model instanceof Chapter) {
$hasPermission = userCan('book-update', $currentParent)
&& userCan('book-update', $newBook)
&& userCan('chapter-update', $model)
&& (!$hasNewParent || userCan('chapter-create', $newBook))
&& (!$hasNewParent || userCan('chapter-delete', $model));
if (!$hasPermission) {
return false;
}
}
if ($model instanceof Page) {
$parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasCurrentParentPermission = userCan($parentPermission, $currentParent);
// This needs to check if there was an intended chapter location in the original sort map
// rather than inferring from the $newChapter since that variable may be null
// due to other reasons (Visibility).
$newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;
if (!$newParent) {
return false;
}
$hasPageEditPermission = userCan('page-update', $model);
$newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id));
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasNewParentPermission = userCan($newParentPermission, $newParent);
$hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model));
$hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent));
$hasPermission = $hasCurrentParentPermission
&& $newParentInRightLocation
&& $hasNewParentPermission
&& $hasPageEditPermission
&& $hasDeletePermissionIfMoving
&& $hasCreatePermissionIfMoving;
if (!$hasPermission) {
return false;
}
}
return true;
}
/**
* Load models from the database into the given sort map.
*
* @return array<string, Entity>
*/
protected function loadModelsFromSortMap(BookSortMap $sortMap): array
{
$modelMap = [];
$ids = [
'chapter' => [],
'page' => [],
'book' => [],
];
foreach ($sortMap->all() as $sortMapItem) {
$ids[$sortMapItem->type][] = $sortMapItem->id;
$ids['book'][] = $sortMapItem->parentBookId;
if ($sortMapItem->parentChapterId) {
$ids['chapter'][] = $sortMapItem->parentChapterId;
}
}
$pages = $this->queries->pages->visibleForList()->whereIn('id', array_unique($ids['page']))->get();
/** @var Page $page */
foreach ($pages as $page) {
$modelMap['page:' . $page->id] = $page;
$ids['book'][] = $page->book_id;
if ($page->chapter_id) {
$ids['chapter'][] = $page->chapter_id;
}
}
$chapters = $this->queries->chapters->visibleForList()->whereIn('id', array_unique($ids['chapter']))->get();
/** @var Chapter $chapter */
foreach ($chapters as $chapter) {
$modelMap['chapter:' . $chapter->id] = $chapter;
$ids['book'][] = $chapter->book_id;
}
$books = $this->queries->books->visibleForList()->whereIn('id', array_unique($ids['book']))->get();
/** @var Book $book */
foreach ($books as $book) {
$modelMap['book:' . $book->id] = $book;
}
return $modelMap;
}
}

63
app/Sorting/SortRule.php Normal file
View file

@ -0,0 +1,63 @@
<?php
namespace BookStack\Sorting;
use BookStack\Activity\Models\Loggable;
use BookStack\Entities\Models\Book;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $name
* @property string $sequence
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class SortRule extends Model implements Loggable
{
use HasFactory;
/**
* @return SortRuleOperation[]
*/
public function getOperations(): array
{
return SortRuleOperation::fromSequence($this->sequence);
}
/**
* @param SortRuleOperation[] $options
*/
public function setOperations(array $options): void
{
$values = array_map(fn (SortRuleOperation $opt) => $opt->value, $options);
$this->sequence = implode(',', $values);
}
public function logDescriptor(): string
{
return "({$this->id}) {$this->name}";
}
public function getUrl(): string
{
return url("/settings/sorting/rules/{$this->id}");
}
public function books(): HasMany
{
return $this->hasMany(Book::class);
}
public static function allByName(): Collection
{
return static::query()
->withCount('books')
->orderBy('name', 'asc')
->get();
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace BookStack\Sorting;
use BookStack\Activity\ActivityType;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
class SortRuleController extends Controller
{
public function __construct()
{
$this->middleware('can:settings-manage');
}
public function create()
{
$this->setPageTitle(trans('settings.sort_rule_create'));
return view('settings.sort-rules.create');
}
public function store(Request $request)
{
$this->validate($request, [
'name' => ['required', 'string', 'min:1', 'max:200'],
'sequence' => ['required', 'string', 'min:1'],
]);
$operations = SortRuleOperation::fromSequence($request->input('sequence'));
if (count($operations) === 0) {
return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']);
}
$rule = new SortRule();
$rule->name = $request->input('name');
$rule->setOperations($operations);
$rule->save();
$this->logActivity(ActivityType::SORT_RULE_CREATE, $rule);
return redirect('/settings/sorting');
}
public function edit(string $id)
{
$rule = SortRule::query()->findOrFail($id);
$this->setPageTitle(trans('settings.sort_rule_edit'));
return view('settings.sort-rules.edit', ['rule' => $rule]);
}
public function update(string $id, Request $request, BookSorter $bookSorter)
{
$this->validate($request, [
'name' => ['required', 'string', 'min:1', 'max:200'],
'sequence' => ['required', 'string', 'min:1'],
]);
$rule = SortRule::query()->findOrFail($id);
$operations = SortRuleOperation::fromSequence($request->input('sequence'));
if (count($operations) === 0) {
return redirect($rule->getUrl())->withInput()->withErrors(['sequence' => 'No operations set.']);
}
$rule->name = $request->input('name');
$rule->setOperations($operations);
$changedSequence = $rule->isDirty('sequence');
$rule->save();
$this->logActivity(ActivityType::SORT_RULE_UPDATE, $rule);
if ($changedSequence) {
$bookSorter->runBookAutoSortForAllWithSet($rule);
}
return redirect('/settings/sorting');
}
public function destroy(string $id, Request $request)
{
$rule = SortRule::query()->findOrFail($id);
$confirmed = $request->input('confirm') === 'true';
$booksAssigned = $rule->books()->count();
$warnings = [];
if ($booksAssigned > 0) {
if ($confirmed) {
$rule->books()->update(['sort_rule_id' => null]);
} else {
$warnings[] = trans('settings.sort_rule_delete_warn_books', ['count' => $booksAssigned]);
}
}
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
if ($defaultBookSortSetting === intval($id)) {
if ($confirmed) {
setting()->remove('sorting-book-default');
} else {
$warnings[] = trans('settings.sort_rule_delete_warn_default');
}
}
if (count($warnings) > 0) {
return redirect($rule->getUrl() . '#delete')->withErrors(['delete' => $warnings]);
}
$rule->delete();
$this->logActivity(ActivityType::SORT_RULE_DELETE, $rule);
return redirect('/settings/sorting');
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace BookStack\Sorting;
use Closure;
use Illuminate\Support\Str;
enum SortRuleOperation: string
{
case NameAsc = 'name_asc';
case NameDesc = 'name_desc';
case NameNumericAsc = 'name_numeric_asc';
case NameNumericDesc = 'name_numeric_desc';
case CreatedDateAsc = 'created_date_asc';
case CreatedDateDesc = 'created_date_desc';
case UpdateDateAsc = 'updated_date_asc';
case UpdateDateDesc = 'updated_date_desc';
case ChaptersFirst = 'chapters_first';
case ChaptersLast = 'chapters_last';
/**
* Provide a translated label string for this option.
*/
public function getLabel(): string
{
$key = $this->value;
$label = '';
if (str_ends_with($key, '_asc')) {
$key = substr($key, 0, -4);
$label = trans('settings.sort_rule_op_asc');
} elseif (str_ends_with($key, '_desc')) {
$key = substr($key, 0, -5);
$label = trans('settings.sort_rule_op_desc');
}
$label = trans('settings.sort_rule_op_' . $key) . ' ' . $label;
return trim($label);
}
public function getSortFunction(): callable
{
$camelValue = Str::camel($this->value);
return SortSetOperationComparisons::$camelValue(...);
}
/**
* @return SortRuleOperation[]
*/
public static function allExcluding(array $operations): array
{
$all = SortRuleOperation::cases();
$filtered = array_filter($all, function (SortRuleOperation $operation) use ($operations) {
return !in_array($operation, $operations);
});
return array_values($filtered);
}
/**
* Create a set of operations from a string sequence representation.
* (values seperated by commas).
* @return SortRuleOperation[]
*/
public static function fromSequence(string $sequence): array
{
$strOptions = explode(',', $sequence);
$options = array_map(fn ($val) => SortRuleOperation::tryFrom($val), $strOptions);
return array_filter($options);
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace BookStack\Sorting;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
/**
* Sort comparison function for each of the possible SortSetOperation values.
* Method names should be camelCase names for the SortSetOperation enum value.
*/
class SortSetOperationComparisons
{
public static function nameAsc(Entity $a, Entity $b): int
{
return strtolower($a->name) <=> strtolower($b->name);
}
public static function nameDesc(Entity $a, Entity $b): int
{
return strtolower($b->name) <=> strtolower($a->name);
}
public static function nameNumericAsc(Entity $a, Entity $b): int
{
$numRegex = '/^\d+(\.\d+)?/';
$aMatches = [];
$bMatches = [];
preg_match($numRegex, $a->name, $aMatches);
preg_match($numRegex, $b->name, $bMatches);
$aVal = floatval(($aMatches[0] ?? 0));
$bVal = floatval(($bMatches[0] ?? 0));
return $aVal <=> $bVal;
}
public static function nameNumericDesc(Entity $a, Entity $b): int
{
return -(static::nameNumericAsc($a, $b));
}
public static function createdDateAsc(Entity $a, Entity $b): int
{
return $a->created_at->unix() <=> $b->created_at->unix();
}
public static function createdDateDesc(Entity $a, Entity $b): int
{
return $b->created_at->unix() <=> $a->created_at->unix();
}
public static function updatedDateAsc(Entity $a, Entity $b): int
{
return $a->updated_at->unix() <=> $b->updated_at->unix();
}
public static function updatedDateDesc(Entity $a, Entity $b): int
{
return $b->updated_at->unix() <=> $a->updated_at->unix();
}
public static function chaptersFirst(Entity $a, Entity $b): int
{
return ($b instanceof Chapter ? 1 : 0) - (($a instanceof Chapter) ? 1 : 0);
}
public static function chaptersLast(Entity $a, Entity $b): int
{
return ($a instanceof Chapter ? 1 : 0) - (($b instanceof Chapter) ? 1 : 0);
}
}

49
app/Sorting/SortUrl.php Normal file
View file

@ -0,0 +1,49 @@
<?php
namespace BookStack\Sorting;
/**
* Generate a URL with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
*/
class SortUrl
{
public function __construct(
protected string $path,
protected array $data,
protected array $overrideData = []
) {
}
public function withOverrideData(array $overrideData = []): self
{
return new self($this->path, $this->data, $overrideData);
}
public function build(): string
{
$queryStringSections = [];
$queryData = array_merge($this->data, $this->overrideData);
// Change sorting direction if already sorted on current attribute
if (isset($this->overrideData['sort']) && $this->overrideData['sort'] === $this->data['sort']) {
$queryData['order'] = ($this->data['order'] === 'asc') ? 'desc' : 'asc';
} elseif (isset($this->overrideData['sort'])) {
$queryData['order'] = 'asc';
}
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal !== '') {
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
}
if (count($queryStringSections) === 0) {
return url($this->path);
}
return url($this->path . '?' . implode('&', $queryStringSections));
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace BookStack\Theming;
use BookStack\Facades\Theme;
use BookStack\Http\Controller;
use BookStack\Util\FilePathNormalizer;
class ThemeController extends Controller
{
/**
* Serve a public file from the configured theme.
*/
public function publicFile(string $theme, string $path)
{
$cleanPath = FilePathNormalizer::normalize($path);
if ($theme !== Theme::getTheme() || !$cleanPath) {
abort(404);
}
$filePath = theme_path("public/{$cleanPath}");
if (!file_exists($filePath)) {
abort(404);
}
$response = $this->download()->streamedFileInline($filePath);
$response->setMaxAge(86400);
return $response;
}
}

View file

@ -15,6 +15,15 @@ class ThemeService
*/ */
protected array $listeners = []; protected array $listeners = [];
/**
* Get the currently configured theme.
* Returns an empty string if not configured.
*/
public function getTheme(): string
{
return config('view.theme') ?? '';
}
/** /**
* Listen to a given custom theme event, * Listen to a given custom theme event,
* setting up the action to be ran when the event occurs. * setting up the action to be ran when the event occurs.
@ -84,7 +93,7 @@ class ThemeService
/** /**
* @see SocialDriverManager::addSocialDriver * @see SocialDriverManager::addSocialDriver
*/ */
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, ?callable $configureForRedirect = null): void
{ {
$driverManager = app()->make(SocialDriverManager::class); $driverManager = app()->make(SocialDriverManager::class);
$driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect); $driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);

View file

@ -47,6 +47,7 @@ class LocaleManager
'ja' => 'ja', 'ja' => 'ja',
'ka' => 'ka_GE', 'ka' => 'ka_GE',
'ko' => 'ko_KR', 'ko' => 'ko_KR',
'ku' => 'ku_TR',
'lt' => 'lt_LT', 'lt' => 'lt_LT',
'lv' => 'lv_LV', 'lv' => 'lv_LV',
'nb' => 'nb_NO', 'nb' => 'nb_NO',

View file

@ -3,13 +3,12 @@
namespace BookStack\Uploads; namespace BookStack\Uploads;
use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\FileUploadException;
use BookStack\Util\FilePathNormalizer;
use Exception; use Exception;
use Illuminate\Contracts\Filesystem\Filesystem as Storage; use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Filesystem\FilesystemManager; use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Flysystem\WhitespacePathNormalizer;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
class FileStorage class FileStorage
@ -121,12 +120,13 @@ class FileStorage
*/ */
protected function adjustPathForStorageDisk(string $path): string protected function adjustPathForStorageDisk(string $path): string
{ {
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path)); $trimmed = str_replace('uploads/files/', '', $path);
$normalized = FilePathNormalizer::normalize($trimmed);
if ($this->getStorageDiskName() === 'local_secure_attachments') { if ($this->getStorageDiskName() === 'local_secure_attachments') {
return $path; return $normalized;
} }
return 'uploads/files/' . $path; return 'uploads/files/' . $normalized;
} }
} }

View file

@ -49,9 +49,9 @@ class ImageRepo
string $type, string $type,
int $page = 0, int $page = 0,
int $pageSize = 24, int $pageSize = 24,
int $uploadedTo = null, ?int $uploadedTo = null,
string $search = null, ?string $search = null,
callable $whereClause = null ?callable $whereClause = null
): array { ): array {
$imageQuery = Image::query()->where('type', '=', strtolower($type)); $imageQuery = Image::query()->where('type', '=', strtolower($type));
@ -91,7 +91,7 @@ class ImageRepo
$parentFilter = function (Builder $query) use ($filterType, $contextPage) { $parentFilter = function (Builder $query) use ($filterType, $contextPage) {
if ($filterType === 'page') { if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id); $query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') { } else if ($filterType === 'book') {
$validPageIds = $contextPage->book->pages() $validPageIds = $contextPage->book->pages()
->scopes('visible') ->scopes('visible')
->pluck('id') ->pluck('id')
@ -109,8 +109,14 @@ class ImageRepo
* *
* @throws ImageUploadException * @throws ImageUploadException
*/ */
public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image public function saveNew(
{ UploadedFile $uploadFile,
string $type,
int $uploadedTo = 0,
?int $resizeWidth = null,
?int $resizeHeight = null,
bool $keepRatio = true
): Image {
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio); $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
if ($type !== 'system') { if ($type !== 'system') {
@ -184,7 +190,7 @@ class ImageRepo
* *
* @throws Exception * @throws Exception
*/ */
public function destroyImage(Image $image = null): void public function destroyImage(?Image $image = null): void
{ {
if ($image) { if ($image) {
$this->imageService->destroy($image); $this->imageService->destroy($image);

View file

@ -158,7 +158,10 @@ class ImageResizer
*/ */
protected function interventionFromImageData(string $imageData, ?string $fileType): InterventionImage protected function interventionFromImageData(string $imageData, ?string $fileType): InterventionImage
{ {
$manager = new ImageManager(new Driver()); $manager = new ImageManager(
new Driver(),
autoOrientation: false,
);
// Ensure gif images are decoded natively instead of deferring to intervention GIF // Ensure gif images are decoded natively instead of deferring to intervention GIF
// handling since we don't need the added animation support. // handling since we don't need the added animation support.

View file

@ -31,8 +31,8 @@ class ImageService
UploadedFile $uploadedFile, UploadedFile $uploadedFile,
string $type, string $type,
int $uploadedTo = 0, int $uploadedTo = 0,
int $resizeWidth = null, ?int $resizeWidth = null,
int $resizeHeight = null, ?int $resizeHeight = null,
bool $keepRatio = true, bool $keepRatio = true,
string $imageName = '', string $imageName = '',
): Image { ): Image {

View file

@ -2,9 +2,9 @@
namespace BookStack\Uploads; namespace BookStack\Uploads;
use BookStack\Util\FilePathNormalizer;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemAdapter;
use League\Flysystem\WhitespacePathNormalizer;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
class ImageStorageDisk class ImageStorageDisk
@ -30,13 +30,14 @@ class ImageStorageDisk
*/ */
protected function adjustPathForDisk(string $path): string protected function adjustPathForDisk(string $path): string
{ {
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path)); $trimmed = str_replace('uploads/images/', '', $path);
$normalized = FilePathNormalizer::normalize($trimmed);
if ($this->usingSecureImages()) { if ($this->usingSecureImages()) {
return $path; return $normalized;
} }
return 'uploads/images/' . $path; return 'uploads/images/' . $normalized;
} }
/** /**

View file

@ -33,7 +33,7 @@ class UserApiController extends ApiController
}); });
} }
protected function rules(int $userId = null): array protected function rules(?int $userId = null): array
{ {
return [ return [
'create' => [ 'create' => [
@ -54,7 +54,7 @@ class UserApiController extends ApiController
'string', 'string',
'email', 'email',
'min:2', 'min:2',
(new Unique('users', 'email'))->ignore($userId ?? null), (new Unique('users', 'email'))->ignore($userId),
], ],
'external_auth_id' => ['string'], 'external_auth_id' => ['string'],
'language' => ['string', 'max:15', 'alpha_dash'], 'language' => ['string', 'max:15', 'alpha_dash'],

View file

@ -0,0 +1,17 @@
<?php
namespace BookStack\Util;
use League\Flysystem\WhitespacePathNormalizer;
/**
* Utility to normalize (potentially) user provided file paths
* to avoid things like directory traversal.
*/
class FilePathNormalizer
{
public static function normalize(string $path): string
{
return (new WhitespacePathNormalizer())->normalizePath($path);
}
}

View file

@ -4,11 +4,16 @@ namespace BookStack\Util;
use BookStack\Exceptions\HttpFetchException; use BookStack\Exceptions\HttpFetchException;
/**
* Validate the host we're connecting to when making a server-side-request.
* Will use the given hosts config if given during construction otherwise
* will look to the app configured config.
*/
class SsrUrlValidator class SsrUrlValidator
{ {
protected string $config; protected string $config;
public function __construct(string $config = null) public function __construct(?string $config = null)
{ {
$this->config = $config ?? config('app.ssr_hosts') ?? ''; $this->config = $config ?? config('app.ssr_hosts') ?? '';
} }

View file

@ -13,7 +13,7 @@ class WebSafeMimeSniffer
/** /**
* @var string[] * @var string[]
*/ */
protected $safeMimes = [ protected array $safeMimes = [
'application/json', 'application/json',
'application/octet-stream', 'application/octet-stream',
'application/pdf', 'application/pdf',
@ -48,16 +48,28 @@ class WebSafeMimeSniffer
'video/av1', 'video/av1',
]; ];
protected array $textTypesByExtension = [
'css' => 'text/css',
'js' => 'text/javascript',
'json' => 'application/json',
'csv' => 'text/csv',
];
/** /**
* Sniff the mime-type from the given file content while running the result * Sniff the mime-type from the given file content while running the result
* through an allow-list to ensure a web-safe result. * through an allow-list to ensure a web-safe result.
* Takes the content as a reference since the value may be quite large. * Takes the content as a reference since the value may be quite large.
* Accepts an optional $extension which can be used for further guessing.
*/ */
public function sniff(string &$content): string public function sniff(string &$content, string $extension = ''): string
{ {
$fInfo = new finfo(FILEINFO_MIME_TYPE); $fInfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $fInfo->buffer($content) ?: 'application/octet-stream'; $mime = $fInfo->buffer($content) ?: 'application/octet-stream';
if ($mime === 'text/plain' && $extension) {
$mime = $this->textTypesByExtension[$extension] ?? 'text/plain';
}
if (in_array($mime, $this->safeMimes)) { if (in_array($mime, $this->safeMimes)) {
return $mime; return $mime;
} }

View file

@ -8,7 +8,7 @@
"license": "MIT", "license": "MIT",
"type": "project", "type": "project",
"require": { "require": {
"php": "^8.1.0", "php": "^8.2.0",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
@ -18,12 +18,11 @@
"ext-xml": "*", "ext-xml": "*",
"ext-zip": "*", "ext-zip": "*",
"bacon/bacon-qr-code": "^3.0", "bacon/bacon-qr-code": "^3.0",
"doctrine/dbal": "^3.5", "dompdf/dompdf": "^3.1",
"dompdf/dompdf": "^3.0",
"guzzlehttp/guzzle": "^7.4", "guzzlehttp/guzzle": "^7.4",
"intervention/image": "^3.5", "intervention/image": "^3.5",
"knplabs/knp-snappy": "^1.5", "knplabs/knp-snappy": "^1.5",
"laravel/framework": "^10.48.23", "laravel/framework": "^v11.37",
"laravel/socialite": "^5.10", "laravel/socialite": "^5.10",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"league/commonmark": "^2.3", "league/commonmark": "^2.3",
@ -40,17 +39,17 @@
"socialiteproviders/okta": "^4.2", "socialiteproviders/okta": "^4.2",
"socialiteproviders/twitch": "^5.3", "socialiteproviders/twitch": "^5.3",
"ssddanbrown/htmldiff": "^1.0.2", "ssddanbrown/htmldiff": "^1.0.2",
"ssddanbrown/symfony-mailer": "6.4.x-dev" "ssddanbrown/symfony-mailer": "7.2.x-dev"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.21", "fakerphp/faker": "^1.21",
"itsgoingd/clockwork": "^5.1", "itsgoingd/clockwork": "^5.1",
"mockery/mockery": "^1.5", "mockery/mockery": "^1.5",
"nunomaduro/collision": "^7.0", "nunomaduro/collision": "^8.1",
"larastan/larastan": "^2.7", "larastan/larastan": "^v3.0",
"phpunit/phpunit": "^10.0", "phpunit/phpunit": "^11.5",
"squizlabs/php_codesniffer": "^3.7", "squizlabs/php_codesniffer": "^3.7",
"ssddanbrown/asserthtml": "^3.0" "ssddanbrown/asserthtml": "^3.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -104,7 +103,7 @@
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true, "sort-packages": true,
"platform": { "platform": {
"php": "8.1.0" "php": "8.2.0"
} }
}, },
"extra": { "extra": {

2248
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,9 @@ class BookFactory extends Factory
'name' => $this->faker->sentence(), 'name' => $this->faker->sentence(),
'slug' => Str::random(10), 'slug' => Str::random(10),
'description' => $description, 'description' => $description,
'description_html' => '<p>' . e($description) . '</p>' 'description_html' => '<p>' . e($description) . '</p>',
'sort_rule_id' => null,
'default_template_id' => null,
]; ];
} }
} }

View file

@ -0,0 +1,30 @@
<?php
namespace Database\Factories\Sorting;
use BookStack\Sorting\SortRule;
use BookStack\Sorting\SortRuleOperation;
use Illuminate\Database\Eloquent\Factories\Factory;
class SortRuleFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = SortRule::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
$cases = SortRuleOperation::cases();
$op = $cases[array_rand($cases)];
return [
'name' => $op->name . ' Sort',
'sequence' => $op->value,
];
}
}

View file

@ -26,25 +26,19 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
$sm = Schema::getConnection()->getDoctrineSchemaManager(); if (Schema::hasIndex('pages', 'search')) {
$prefix = DB::getTablePrefix();
$pages = $sm->introspectTable($prefix . 'pages');
$books = $sm->introspectTable($prefix . 'books');
$chapters = $sm->introspectTable($prefix . 'chapters');
if ($pages->hasIndex('search')) {
Schema::table('pages', function (Blueprint $table) { Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('search'); $table->dropIndex('search');
}); });
} }
if ($books->hasIndex('search')) { if (Schema::hasIndex('books', 'search')) {
Schema::table('books', function (Blueprint $table) { Schema::table('books', function (Blueprint $table) {
$table->dropIndex('search'); $table->dropIndex('search');
}); });
} }
if ($chapters->hasIndex('search')) { if (Schema::hasIndex('chapters', 'search')) {
Schema::table('chapters', function (Blueprint $table) { Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('search'); $table->dropIndex('search');
}); });

View file

@ -26,25 +26,19 @@ return new class extends Migration
*/ */
public function down(): void public function down(): void
{ {
$sm = Schema::getConnection()->getDoctrineSchemaManager(); if (Schema::hasIndex('pages', 'name_search')) {
$prefix = DB::getTablePrefix();
$pages = $sm->introspectTable($prefix . 'pages');
$books = $sm->introspectTable($prefix . 'books');
$chapters = $sm->introspectTable($prefix . 'chapters');
if ($pages->hasIndex('name_search')) {
Schema::table('pages', function (Blueprint $table) { Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('name_search'); $table->dropIndex('name_search');
}); });
} }
if ($books->hasIndex('name_search')) { if (Schema::hasIndex('books', 'name_search')) {
Schema::table('books', function (Blueprint $table) { Schema::table('books', function (Blueprint $table) {
$table->dropIndex('name_search'); $table->dropIndex('name_search');
}); });
} }
if ($chapters->hasIndex('name_search')) { if (Schema::hasIndex('chapters', 'name_search')) {
Schema::table('chapters', function (Blueprint $table) { Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('name_search'); $table->dropIndex('name_search');
}); });

View file

@ -25,27 +25,21 @@ return new class extends Migration
$table->index('score'); $table->index('score');
}); });
$sm = Schema::getConnection()->getDoctrineSchemaManager(); if (Schema::hasIndex('pages', 'search')) {
$prefix = DB::getTablePrefix();
$pages = $sm->introspectTable($prefix . 'pages');
$books = $sm->introspectTable($prefix . 'books');
$chapters = $sm->introspectTable($prefix . 'chapters');
if ($pages->hasIndex('search')) {
Schema::table('pages', function (Blueprint $table) { Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('search'); $table->dropIndex('search');
$table->dropIndex('name_search'); $table->dropIndex('name_search');
}); });
} }
if ($books->hasIndex('search')) { if (Schema::hasIndex('books', 'search')) {
Schema::table('books', function (Blueprint $table) { Schema::table('books', function (Blueprint $table) {
$table->dropIndex('search'); $table->dropIndex('search');
$table->dropIndex('name_search'); $table->dropIndex('name_search');
}); });
} }
if ($chapters->hasIndex('search')) { if (Schema::hasIndex('chapters', 'search')) {
Schema::table('chapters', function (Blueprint $table) { Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('search'); $table->dropIndex('search');
$table->dropIndex('name_search'); $table->dropIndex('name_search');

View file

@ -8,7 +8,7 @@ return new class extends Migration
/** /**
* Mapping of old polymorphic types to new simpler values. * Mapping of old polymorphic types to new simpler values.
*/ */
protected $changeMap = [ protected array $changeMap = [
'BookStack\\Bookshelf' => 'bookshelf', 'BookStack\\Bookshelf' => 'bookshelf',
'BookStack\\Book' => 'book', 'BookStack\\Book' => 'book',
'BookStack\\Chapter' => 'chapter', 'BookStack\\Chapter' => 'chapter',
@ -18,7 +18,7 @@ return new class extends Migration
/** /**
* Mapping of tables and columns that contain polymorphic types. * Mapping of tables and columns that contain polymorphic types.
*/ */
protected $columnsByTable = [ protected array $columnsByTable = [
'activities' => 'entity_type', 'activities' => 'entity_type',
'comments' => 'entity_type', 'comments' => 'entity_type',
'deletions' => 'deletable_type', 'deletions' => 'deletable_type',

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sort_rules', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->text('sequence');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sort_rules');
}
};

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('books', function (Blueprint $table) {
$table->unsignedInteger('sort_rule_id')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('books', function (Blueprint $table) {
$table->dropColumn('sort_rule_id');
});
}
};

View file

@ -2,7 +2,9 @@
BookStack allows logical customization via the theme system which enables you to add, or extend, functionality within the PHP side of the system without needing to alter the core application files. BookStack allows logical customization via the theme system which enables you to add, or extend, functionality within the PHP side of the system without needing to alter the core application files.
WARNING: This system is currently in alpha so may incur changes. Once we've gathered some feedback on usage we'll look to removing this warning. This system will be considered semi-stable in the future. The `Theme::` system will be kept maintained but specific customizations or deeper app/framework usage using this system will not be supported nor considered in any way stable. Customizations using this system should be checked after updates. This is part of the theme system alongside the [visual theme system](./visual-theme-system.md).
**Note:** This system is considered semi-stable. The `Theme::` system is kept maintained but specific customizations or deeper app/framework usage using this system will not be supported nor considered in any way stable. Customizations using this system should be checked after updates.
## Getting Started ## Getting Started
@ -47,6 +49,7 @@ This method allows you to register a custom social authentication driver within
- string $driverName - string $driverName
- array $config - array $config
- string $socialiteHandler - string $socialiteHandler
- callable|null $configureForRedirect = null
**Example** **Example**

View file

@ -2,7 +2,9 @@
BookStack allows visual customization via the theme system which enables you to extensively customize views, translation text & icons. BookStack allows visual customization via the theme system which enables you to extensively customize views, translation text & icons.
This theme system itself is maintained and supported but usages of this system, including the files you are able to override, are not considered stable and may change upon any update. You should test any customizations made after updates. This is part of the theme system alongside the [logical theme system](./logical-theme-system.md).
**Note:** This theme system itself is maintained and supported but usages of this system, including the files you are able to override, are not considered stable and may change upon any update. You should test any customizations made after updates.
## Getting Started ## Getting Started
@ -32,3 +34,24 @@ return [
'search' => 'find', 'search' => 'find',
]; ];
``` ```
## Publicly Accessible Files
As part of deeper customizations you may want to expose additional files
(images, scripts, styles, etc...) as part of your theme, in a way so they're
accessible in public web-space to browsers.
To achieve this, you can put files within a `themes/<theme_name>/public` folder.
BookStack will serve any files within this folder from a `/theme/<theme_name>` base path.
As an example, if I had an image located at `themes/custom/public/cat.jpg`, I could access
that image via the URL path `/theme/custom/cat.jpg`. That's assuming that `custom` is the currently
configured application theme.
There are some considerations to these publicly served files:
- Only a predetermined range "web safe" content-types are currently served.
- This limits running into potential insecure scenarios in serving problematic file types.
- A static 1-day cache time it set on files served from this folder.
- You can use alternative cache-breaking techniques (change of query string) upon changes if needed.
- If required, you could likely override caching at the webserver level.

View file

@ -127,6 +127,13 @@ Copyright: Copyright (c) 2023 ECMAScript Shims
Source: git+https://github.com/es-shims/ArrayBuffer.prototype.slice.git Source: git+https://github.com/es-shims/ArrayBuffer.prototype.slice.git
Link: https://github.com/es-shims/ArrayBuffer.prototype.slice#readme Link: https://github.com/es-shims/ArrayBuffer.prototype.slice#readme
----------- -----------
async-function
License: MIT
License File: node_modules/async-function/LICENSE
Copyright: Copyright (c) 2016 EduardoRFS
Source: git+https://github.com/ljharb/async-function.git
Link: https://github.com/ljharb/async-function#readme
-----------
async async
License: MIT License: MIT
License File: node_modules/async/LICENSE License File: node_modules/async/LICENSE
@ -239,6 +246,13 @@ Copyright: Copyright (c) 2016, 2018 Linus Unnebäck
Source: LinusU/buffer-from Source: LinusU/buffer-from
Link: LinusU/buffer-from Link: LinusU/buffer-from
----------- -----------
call-bind-apply-helpers
License: MIT
License File: node_modules/call-bind-apply-helpers/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/call-bind-apply-helpers.git
Link: https://github.com/ljharb/call-bind-apply-helpers#readme
-----------
call-bind call-bind
License: MIT License: MIT
License File: node_modules/call-bind/LICENSE License File: node_modules/call-bind/LICENSE
@ -246,6 +260,13 @@ Copyright: Copyright (c) 2020 Jordan Harband
Source: git+https://github.com/ljharb/call-bind.git Source: git+https://github.com/ljharb/call-bind.git
Link: https://github.com/ljharb/call-bind#readme Link: https://github.com/ljharb/call-bind#readme
----------- -----------
call-bound
License: MIT
License File: node_modules/call-bound/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/call-bound.git
Link: https://github.com/ljharb/call-bound#readme
-----------
callsites callsites
License: MIT License: MIT
License File: node_modules/callsites/license License File: node_modules/callsites/license
@ -303,6 +324,7 @@ Link: https://github.com/watson/ci-info
cjs-module-lexer cjs-module-lexer
License: MIT License: MIT
License File: node_modules/cjs-module-lexer/LICENSE License File: node_modules/cjs-module-lexer/LICENSE
Copyright: Copyright (C) 2018-2020 Guy Bedford
Source: git+https://github.com/nodejs/cjs-module-lexer.git Source: git+https://github.com/nodejs/cjs-module-lexer.git
Link: https://github.com/nodejs/cjs-module-lexer#readme Link: https://github.com/nodejs/cjs-module-lexer#readme
----------- -----------
@ -360,13 +382,6 @@ License File: node_modules/concat-map/LICENSE
Source: git://github.com/substack/node-concat-map.git Source: git://github.com/substack/node-concat-map.git
Link: git://github.com/substack/node-concat-map.git Link: git://github.com/substack/node-concat-map.git
----------- -----------
confusing-browser-globals
License: MIT
License File: node_modules/confusing-browser-globals/LICENSE
Copyright: Copyright (c) 2013-present, Facebook, Inc.
Source: https://github.com/facebook/create-react-app.git
Link: https://github.com/facebook/create-react-app.git
-----------
convert-source-map convert-source-map
License: MIT License: MIT
License File: node_modules/convert-source-map/LICENSE License File: node_modules/convert-source-map/LICENSE
@ -427,22 +442,22 @@ data-view-buffer
License: MIT License: MIT
License File: node_modules/data-view-buffer/LICENSE License File: node_modules/data-view-buffer/LICENSE
Copyright: Copyright (c) 2023 Jordan Harband Copyright: Copyright (c) 2023 Jordan Harband
Source: git+https://github.com/ljharb/data-view-buffer.git Source: git+https://github.com/inspect-js/data-view-buffer.git
Link: https://github.com/ljharb/data-view-buffer#readme Link: https://github.com/inspect-js/data-view-buffer#readme
----------- -----------
data-view-byte-length data-view-byte-length
License: MIT License: MIT
License File: node_modules/data-view-byte-length/LICENSE License File: node_modules/data-view-byte-length/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/data-view-byte-length.git Source: git+https://github.com/inspect-js/data-view-byte-length.git
Link: https://github.com/ljharb/data-view-byte-length#readme Link: https://github.com/inspect-js/data-view-byte-length#readme
----------- -----------
data-view-byte-offset data-view-byte-offset
License: MIT License: MIT
License File: node_modules/data-view-byte-offset/LICENSE License File: node_modules/data-view-byte-offset/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/data-view-byte-offset.git Source: git+https://github.com/inspect-js/data-view-byte-offset.git
Link: https://github.com/ljharb/data-view-byte-offset#readme Link: https://github.com/inspect-js/data-view-byte-offset#readme
----------- -----------
debug debug
License: MIT License: MIT
@ -546,6 +561,13 @@ License File: node_modules/domexception/LICENSE.txt
Source: jsdom/domexception Source: jsdom/domexception
Link: jsdom/domexception Link: jsdom/domexception
----------- -----------
dunder-proto
License: MIT
License File: node_modules/dunder-proto/LICENSE
Copyright: Copyright (c) 2024 ECMAScript Shims
Source: git+https://github.com/es-shims/dunder-proto.git
Link: https://github.com/es-shims/dunder-proto#readme
-----------
ejs ejs
License: Apache-2.0 License: Apache-2.0
License File: node_modules/ejs/LICENSE License File: node_modules/ejs/LICENSE
@ -664,13 +686,6 @@ Copyright: Copyright (C) 2012 Yusuke Suzuki (twitter: @Constellation) and other
Source: http://github.com/estools/escodegen.git Source: http://github.com/estools/escodegen.git
Link: http://github.com/estools/escodegen Link: http://github.com/estools/escodegen
----------- -----------
eslint-config-airbnb-base
License: MIT
License File: node_modules/eslint-config-airbnb-base/LICENSE.md
Copyright: Copyright (c) 2012 Airbnb
Source: https://github.com/airbnb/javascript
Link: https://github.com/airbnb/javascript
-----------
eslint-import-resolver-node eslint-import-resolver-node
License: MIT License: MIT
License File: node_modules/eslint-import-resolver-node/LICENSE License File: node_modules/eslint-import-resolver-node/LICENSE
@ -696,14 +711,14 @@ eslint-scope
License: BSD-2-Clause License: BSD-2-Clause
License File: node_modules/eslint-scope/LICENSE License File: node_modules/eslint-scope/LICENSE
Copyright: Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors. Copyright: Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors.
Source: eslint/eslint-scope Source: eslint/js
Link: http://github.com/eslint/eslint-scope Link: https://github.com/eslint/js/blob/main/packages/eslint-scope/README.md
----------- -----------
eslint-visitor-keys eslint-visitor-keys
License: Apache-2.0 License: Apache-2.0
License File: node_modules/eslint-visitor-keys/LICENSE License File: node_modules/eslint-visitor-keys/LICENSE
Source: eslint/eslint-visitor-keys Source: eslint/js
Link: https://github.com/eslint/eslint-visitor-keys#readme Link: https://github.com/eslint/js/blob/main/packages/eslint-visitor-keys/README.md
----------- -----------
eslint eslint
License: MIT License: MIT
@ -716,8 +731,8 @@ License: BSD-2-Clause
License File: node_modules/espree/LICENSE License File: node_modules/espree/LICENSE
Copyright: Copyright (c) Open JS Foundation Copyright: Copyright (c) Open JS Foundation
All rights reserved. All rights reserved.
Source: eslint/espree Source: eslint/js
Link: https://github.com/eslint/espree Link: https://github.com/eslint/js/blob/main/packages/espree/README.md
----------- -----------
esprima esprima
License: BSD-2-Clause License: BSD-2-Clause
@ -790,13 +805,6 @@ Copyright: Copyright (c) 2013 [Ramesh Nair](http://www.hiddentao.com/)
Source: https://github.com/hiddentao/fast-levenshtein.git Source: https://github.com/hiddentao/fast-levenshtein.git
Link: https://github.com/hiddentao/fast-levenshtein.git Link: https://github.com/hiddentao/fast-levenshtein.git
----------- -----------
fastq
License: ISC
License File: node_modules/fastq/LICENSE
Copyright: Copyright (c) 2015-2020, Matteo Collina <******.*******@*****.***>
Source: git+https://github.com/mcollina/fastq.git
Link: https://github.com/mcollina/fastq#readme
-----------
fb-watchman fb-watchman
License: Apache-2.0 License: Apache-2.0
Source: git@github.com:facebook/watchman.git Source: git@github.com:facebook/watchman.git
@ -805,9 +813,9 @@ Link: https://facebook.github.io/watchman/
file-entry-cache file-entry-cache
License: MIT License: MIT
License File: node_modules/file-entry-cache/LICENSE License File: node_modules/file-entry-cache/LICENSE
Copyright: Copyright (c) 2015 Roy Riojas Copyright: Copyright (c) Roy Riojas & Jared Wray
Source: royriojas/file-entry-cache Source: jaredwray/file-entry-cache
Link: royriojas/file-entry-cache Link: jaredwray/file-entry-cache
----------- -----------
filelist filelist
License: Apache-2.0 License: Apache-2.0
@ -846,7 +854,7 @@ for-each
License: MIT License: MIT
License File: node_modules/for-each/LICENSE License File: node_modules/for-each/LICENSE
Copyright: Copyright (c) 2012 Raynos. Copyright: Copyright (c) 2012 Raynos.
Source: git://github.com/Raynos/for-each.git Source: https://github.com/Raynos/for-each.git
Link: https://github.com/Raynos/for-each Link: https://github.com/Raynos/for-each
----------- -----------
form-data form-data
@ -912,6 +920,13 @@ Copyright: Copyright (c) 2020 CFWare, LLC
Source: git+https://github.com/cfware/get-package-type.git Source: git+https://github.com/cfware/get-package-type.git
Link: https://github.com/cfware/get-package-type#readme Link: https://github.com/cfware/get-package-type#readme
----------- -----------
get-proto
License: MIT
License File: node_modules/get-proto/LICENSE
Copyright: Copyright (c) 2025 Jordan Harband
Source: git+https://github.com/ljharb/get-proto.git
Link: https://github.com/ljharb/get-proto#readme
-----------
get-stream get-stream
License: MIT License: MIT
License File: node_modules/get-stream/license License File: node_modules/get-stream/license
@ -968,13 +983,6 @@ Copyright: Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contri
Source: https://github.com/isaacs/node-graceful-fs Source: https://github.com/isaacs/node-graceful-fs
Link: https://github.com/isaacs/node-graceful-fs Link: https://github.com/isaacs/node-graceful-fs
----------- -----------
graphemer
License: MIT
License File: node_modules/graphemer/LICENSE
Copyright: Copyright 2020 Filament (Anomalous Technologies Limited)
Source: https://github.com/flmnt/graphemer.git
Link: https://github.com/flmnt/graphemer
-----------
has-bigints has-bigints
License: MIT License: MIT
License File: node_modules/has-bigints/LICENSE License File: node_modules/has-bigints/LICENSE
@ -1139,6 +1147,13 @@ Copyright: Copyright (c) 2015 JD Ballard
Source: https://github.com/qix-/node-is-arrayish.git Source: https://github.com/qix-/node-is-arrayish.git
Link: https://github.com/qix-/node-is-arrayish.git Link: https://github.com/qix-/node-is-arrayish.git
----------- -----------
is-async-function
License: MIT
License File: node_modules/is-async-function/LICENSE
Copyright: Copyright (c) 2021 Jordan Harband
Source: git://github.com/inspect-js/is-async-function.git
Link: git://github.com/inspect-js/is-async-function.git
-----------
is-bigint is-bigint
License: MIT License: MIT
License File: node_modules/is-bigint/LICENSE License File: node_modules/is-bigint/LICENSE
@ -1195,6 +1210,13 @@ Copyright: Copyright (c) 2014-2016, Jon Schlinkert
Source: jonschlinkert/is-extglob Source: jonschlinkert/is-extglob
Link: https://github.com/jonschlinkert/is-extglob Link: https://github.com/jonschlinkert/is-extglob
----------- -----------
is-finalizationregistry
License: MIT
License File: node_modules/is-finalizationregistry/LICENSE
Copyright: Copyright (c) 2020 Inspect JS
Source: git+https://github.com/inspect-js/is-finalizationregistry.git
Link: https://github.com/inspect-js/is-finalizationregistry#readme
-----------
is-fullwidth-code-point is-fullwidth-code-point
License: MIT License: MIT
License File: node_modules/is-fullwidth-code-point/license License File: node_modules/is-fullwidth-code-point/license
@ -1209,6 +1231,13 @@ Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.co
Source: sindresorhus/is-generator-fn Source: sindresorhus/is-generator-fn
Link: sindresorhus/is-generator-fn Link: sindresorhus/is-generator-fn
----------- -----------
is-generator-function
License: MIT
License File: node_modules/is-generator-function/LICENSE
Copyright: Copyright (c) 2014 Jordan Harband
Source: git://github.com/inspect-js/is-generator-function.git
Link: git://github.com/inspect-js/is-generator-function.git
-----------
is-glob is-glob
License: MIT License: MIT
License File: node_modules/is-glob/LICENSE License File: node_modules/is-glob/LICENSE
@ -1216,12 +1245,12 @@ Copyright: Copyright (c) 2014-2017, Jon Schlinkert.
Source: micromatch/is-glob Source: micromatch/is-glob
Link: https://github.com/micromatch/is-glob Link: https://github.com/micromatch/is-glob
----------- -----------
is-negative-zero is-map
License: MIT License: MIT
License File: node_modules/is-negative-zero/LICENSE License File: node_modules/is-map/LICENSE
Copyright: Copyright (c) 2014 Jordan Harband Copyright: Copyright (c) 2019 Inspect JS
Source: git://github.com/inspect-js/is-negative-zero.git Source: git+https://github.com/inspect-js/is-map.git
Link: https://github.com/inspect-js/is-negative-zero Link: https://github.com/inspect-js/is-map#readme
----------- -----------
is-number-object is-number-object
License: MIT License: MIT
@ -1237,13 +1266,6 @@ Copyright: Copyright (c) 2014-present, Jon Schlinkert.
Source: jonschlinkert/is-number Source: jonschlinkert/is-number
Link: https://github.com/jonschlinkert/is-number Link: https://github.com/jonschlinkert/is-number
----------- -----------
is-path-inside
License: MIT
License File: node_modules/is-path-inside/license
Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com)
Source: sindresorhus/is-path-inside
Link: sindresorhus/is-path-inside
-----------
is-potential-custom-element-name is-potential-custom-element-name
License: MIT License: MIT
License File: node_modules/is-potential-custom-element-name/LICENSE-MIT.txt License File: node_modules/is-potential-custom-element-name/LICENSE-MIT.txt
@ -1257,6 +1279,13 @@ Copyright: Copyright (c) 2014 Jordan Harband
Source: git://github.com/inspect-js/is-regex.git Source: git://github.com/inspect-js/is-regex.git
Link: https://github.com/inspect-js/is-regex Link: https://github.com/inspect-js/is-regex
----------- -----------
is-set
License: MIT
License File: node_modules/is-set/LICENSE
Copyright: Copyright (c) 2019 Inspect JS
Source: git+https://github.com/inspect-js/is-set.git
Link: https://github.com/inspect-js/is-set#readme
-----------
is-shared-array-buffer is-shared-array-buffer
License: MIT License: MIT
License File: node_modules/is-shared-array-buffer/LICENSE License File: node_modules/is-shared-array-buffer/LICENSE
@ -1275,8 +1304,8 @@ is-string
License: MIT License: MIT
License File: node_modules/is-string/LICENSE License File: node_modules/is-string/LICENSE
Copyright: Copyright (c) 2015 Jordan Harband Copyright: Copyright (c) 2015 Jordan Harband
Source: git://github.com/ljharb/is-string.git Source: git://github.com/inspect-js/is-string.git
Link: git://github.com/ljharb/is-string.git Link: git://github.com/inspect-js/is-string.git
----------- -----------
is-symbol is-symbol
License: MIT License: MIT
@ -1292,6 +1321,13 @@ Copyright: Copyright (c) 2015 Jordan Harband
Source: git://github.com/inspect-js/is-typed-array.git Source: git://github.com/inspect-js/is-typed-array.git
Link: git://github.com/inspect-js/is-typed-array.git Link: git://github.com/inspect-js/is-typed-array.git
----------- -----------
is-weakmap
License: MIT
License File: node_modules/is-weakmap/LICENSE
Copyright: Copyright (c) 2019 Inspect JS
Source: git+https://github.com/inspect-js/is-weakmap.git
Link: https://github.com/inspect-js/is-weakmap#readme
-----------
is-weakref is-weakref
License: MIT License: MIT
License File: node_modules/is-weakref/LICENSE License File: node_modules/is-weakref/LICENSE
@ -1299,6 +1335,13 @@ Copyright: Copyright (c) 2020 Inspect JS
Source: git+https://github.com/inspect-js/is-weakref.git Source: git+https://github.com/inspect-js/is-weakref.git
Link: https://github.com/inspect-js/is-weakref#readme Link: https://github.com/inspect-js/is-weakref#readme
----------- -----------
is-weakset
License: MIT
License File: node_modules/is-weakset/LICENSE
Copyright: Copyright (c) 2019 Inspect JS
Source: git+https://github.com/inspect-js/is-weakset.git
Link: https://github.com/inspect-js/is-weakset#readme
-----------
isarray isarray
License: MIT License: MIT
License File: node_modules/isarray/LICENSE License File: node_modules/isarray/LICENSE
@ -1708,6 +1751,13 @@ Copyright: Copyright (c) Isaac Z. Schlueter and Contributors
Source: git://github.com/isaacs/node-lru-cache.git Source: git://github.com/isaacs/node-lru-cache.git
Link: git://github.com/isaacs/node-lru-cache.git Link: git://github.com/isaacs/node-lru-cache.git
----------- -----------
magic-string
License: MIT
License File: node_modules/magic-string/LICENSE
Copyright: Copyright 2018 Rich Harris
Source: https://github.com/rich-harris/magic-string
Link: https://github.com/rich-harris/magic-string
-----------
make-dir make-dir
License: MIT License: MIT
License File: node_modules/make-dir/license License File: node_modules/make-dir/license
@ -1743,6 +1793,13 @@ Copyright: Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.
Source: markdown-it/markdown-it Source: markdown-it/markdown-it
Link: markdown-it/markdown-it Link: markdown-it/markdown-it
----------- -----------
math-intrinsics
License: MIT
License File: node_modules/math-intrinsics/LICENSE
Copyright: Copyright (c) 2024 ECMAScript Shims
Source: git+https://github.com/es-shims/math-intrinsics.git
Link: https://github.com/es-shims/math-intrinsics#readme
-----------
mdurl mdurl
License: MIT License: MIT
License File: node_modules/mdurl/LICENSE License File: node_modules/mdurl/LICENSE
@ -1903,13 +1960,6 @@ Copyright: Copyright (c) 2014 Jordan Harband
Source: git://github.com/ljharb/object.assign.git Source: git://github.com/ljharb/object.assign.git
Link: git://github.com/ljharb/object.assign.git Link: git://github.com/ljharb/object.assign.git
----------- -----------
object.entries
License: MIT
License File: node_modules/object.entries/LICENSE
Copyright: Copyright (c) 2015 Jordan Harband
Source: git://github.com/es-shims/Object.entries.git
Link: git://github.com/es-shims/Object.entries.git
-----------
object.fromentries object.fromentries
License: MIT License: MIT
License File: node_modules/object.fromentries/LICENSE License File: node_modules/object.fromentries/LICENSE
@ -1960,6 +2010,13 @@ All rights reserved.
Source: github:khtdr/opts Source: github:khtdr/opts
Link: http://khtdr.com/opts Link: http://khtdr.com/opts
----------- -----------
own-keys
License: MIT
License File: node_modules/own-keys/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/own-keys.git
Link: https://github.com/ljharb/own-keys#readme
-----------
p-limit p-limit
License: MIT License: MIT
License File: node_modules/p-limit/license License File: node_modules/p-limit/license
@ -2000,7 +2057,7 @@ License: MIT
License File: node_modules/parse5/LICENSE License File: node_modules/parse5/LICENSE
Copyright: Copyright (c) 2013-2019 Ivan Nikulin (******@*****.***, https://github.com/inikulin) Copyright: Copyright (c) 2013-2019 Ivan Nikulin (******@*****.***, https://github.com/inikulin)
Source: git://github.com/inikulin/parse5.git Source: git://github.com/inikulin/parse5.git
Link: https://github.com/inikulin/parse5 Link: https://parse5.js.org
----------- -----------
path-exists path-exists
License: MIT License: MIT
@ -2140,13 +2197,6 @@ Copyright: Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.
Source: https://github.com/unshiftio/querystringify Source: https://github.com/unshiftio/querystringify
Link: https://github.com/unshiftio/querystringify Link: https://github.com/unshiftio/querystringify
----------- -----------
queue-microtask
License: MIT
License File: node_modules/queue-microtask/LICENSE
Copyright: Copyright (c) Feross Aboukhadijeh
Source: git://github.com/feross/queue-microtask.git
Link: https://github.com/feross/queue-microtask
-----------
react-is react-is
License: MIT License: MIT
License File: node_modules/react-is/LICENSE License File: node_modules/react-is/LICENSE
@ -2168,6 +2218,13 @@ Copyright: Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmil
Source: git://github.com/paulmillr/readdirp.git Source: git://github.com/paulmillr/readdirp.git
Link: https://github.com/paulmillr/readdirp Link: https://github.com/paulmillr/readdirp
----------- -----------
reflect.getprototypeof
License: MIT
License File: node_modules/reflect.getprototypeof/LICENSE
Copyright: Copyright (c) 2021 ECMAScript Shims
Source: git+https://github.com/es-shims/Reflect.getPrototypeOf.git
Link: https://github.com/es-shims/Reflect.getPrototypeOf
-----------
regexp.prototype.flags regexp.prototype.flags
License: MIT License: MIT
License File: node_modules/regexp.prototype.flags/LICENSE License File: node_modules/regexp.prototype.flags/LICENSE
@ -2224,26 +2281,17 @@ Copyright: Copyright (c) 2012 James Halliday
Source: git://github.com/browserify/resolve.git Source: git://github.com/browserify/resolve.git
Link: git://github.com/browserify/resolve.git Link: git://github.com/browserify/resolve.git
----------- -----------
reusify rollup-plugin-dts
License: MIT License: LGPL-3.0
License File: node_modules/reusify/LICENSE Source: git+https://github.com/Swatinem/rollup-plugin-dts.git
Copyright: Copyright (c) 2015 Matteo Collina Link: https://github.com/Swatinem/rollup-plugin-dts#readme
Source: git+https://github.com/mcollina/reusify.git
Link: https://github.com/mcollina/reusify#readme
----------- -----------
rimraf rollup
License: ISC
License File: node_modules/rimraf/LICENSE
Copyright: Copyright (c) Isaac Z. Schlueter and Contributors
Source: git://github.com/isaacs/rimraf.git
Link: git://github.com/isaacs/rimraf.git
-----------
run-parallel
License: MIT License: MIT
License File: node_modules/run-parallel/LICENSE License File: node_modules/rollup/LICENSE.md
Copyright: Copyright (c) Feross Aboukhadijeh Copyright: Copyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)
Source: git://github.com/feross/run-parallel.git Source: rollup/rollup
Link: https://github.com/feross/run-parallel Link: https://rollupjs.org/
----------- -----------
safe-array-concat safe-array-concat
License: MIT License: MIT
@ -2252,6 +2300,13 @@ Copyright: Copyright (c) 2023 Jordan Harband
Source: git+https://github.com/ljharb/safe-array-concat.git Source: git+https://github.com/ljharb/safe-array-concat.git
Link: https://github.com/ljharb/safe-array-concat#readme Link: https://github.com/ljharb/safe-array-concat#readme
----------- -----------
safe-push-apply
License: MIT
License File: node_modules/safe-push-apply/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/safe-push-apply.git
Link: https://github.com/ljharb/safe-push-apply#readme
-----------
safe-regex-test safe-regex-test
License: MIT License: MIT
License File: node_modules/safe-regex-test/LICENSE License File: node_modules/safe-regex-test/LICENSE
@ -2306,6 +2361,13 @@ Copyright: Copyright (c) Jordan Harband and contributors
Source: git+https://github.com/ljharb/set-function-name.git Source: git+https://github.com/ljharb/set-function-name.git
Link: https://github.com/ljharb/set-function-name#readme Link: https://github.com/ljharb/set-function-name#readme
----------- -----------
set-proto
License: MIT
License File: node_modules/set-proto/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/set-proto.git
Link: https://github.com/ljharb/set-proto#readme
-----------
shebang-command shebang-command
License: MIT License: MIT
License File: node_modules/shebang-command/license License File: node_modules/shebang-command/license
@ -2327,6 +2389,27 @@ Copyright: Copyright (c) 2013 James Halliday (****@********.***)
Source: http://github.com/ljharb/shell-quote.git Source: http://github.com/ljharb/shell-quote.git
Link: https://github.com/ljharb/shell-quote Link: https://github.com/ljharb/shell-quote
----------- -----------
side-channel-list
License: MIT
License File: node_modules/side-channel-list/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/side-channel-list.git
Link: https://github.com/ljharb/side-channel-list#readme
-----------
side-channel-map
License: MIT
License File: node_modules/side-channel-map/LICENSE
Copyright: Copyright (c) 2024 Jordan Harband
Source: git+https://github.com/ljharb/side-channel-map.git
Link: https://github.com/ljharb/side-channel-map#readme
-----------
side-channel-weakmap
License: MIT
License File: node_modules/side-channel-weakmap/LICENSE
Copyright: Copyright (c) 2019 Jordan Harband
Source: git+https://github.com/ljharb/side-channel-weakmap.git
Link: https://github.com/ljharb/side-channel-weakmap#readme
-----------
side-channel side-channel
License: MIT License: MIT
License File: node_modules/side-channel/LICENSE License File: node_modules/side-channel/LICENSE
@ -2534,12 +2617,6 @@ Copyright: Copyright (c) 2016, Contributors
Source: git+https://github.com/istanbuljs/test-exclude.git Source: git+https://github.com/istanbuljs/test-exclude.git
Link: https://istanbul.js.org/ Link: https://istanbul.js.org/
----------- -----------
text-table
License: MIT
License File: node_modules/text-table/LICENSE
Source: git://github.com/substack/text-table.git
Link: https://github.com/substack/text-table
-----------
tmpl tmpl
License: BSD-3-Clause License: BSD-3-Clause
License File: node_modules/tmpl/license License File: node_modules/tmpl/license
@ -2547,14 +2624,6 @@ Copyright: Copyright (c) 2014, Naitik Shah. All rights reserved.
Source: https://github.com/daaku/nodejs-tmpl Source: https://github.com/daaku/nodejs-tmpl
Link: https://github.com/daaku/nodejs-tmpl Link: https://github.com/daaku/nodejs-tmpl
----------- -----------
to-fast-properties
License: MIT
License File: node_modules/to-fast-properties/license
Copyright: Copyright (c) 2014 Petka Antonov
2015 Sindre Sorhus
Source: sindresorhus/to-fast-properties
Link: sindresorhus/to-fast-properties
-----------
to-regex-range to-regex-range
License: MIT License: MIT
License File: node_modules/to-regex-range/LICENSE License File: node_modules/to-regex-range/LICENSE
@ -2579,7 +2648,7 @@ Link: https://github.com/jsdom/tr46
ts-jest ts-jest
License: MIT License: MIT
License File: node_modules/ts-jest/LICENSE.md License File: node_modules/ts-jest/LICENSE.md
Copyright: Copyright (c) 2016-2018 Copyright: Copyright (c) 2016-2025
Source: git+https://github.com/kulshekhar/ts-jest.git Source: git+https://github.com/kulshekhar/ts-jest.git
Link: https://kulshekhar.github.io/ts-jest Link: https://kulshekhar.github.io/ts-jest
----------- -----------
@ -2622,8 +2691,8 @@ typed-array-buffer
License: MIT License: MIT
License File: node_modules/typed-array-buffer/LICENSE License File: node_modules/typed-array-buffer/LICENSE
Copyright: Copyright (c) 2023 Jordan Harband Copyright: Copyright (c) 2023 Jordan Harband
Source: git+https://github.com/ljharb/typed-array-buffer.git Source: git+https://github.com/inspect-js/typed-array-buffer.git
Link: https://github.com/ljharb/typed-array-buffer#readme Link: https://github.com/inspect-js/typed-array-buffer#readme
----------- -----------
typed-array-byte-length typed-array-byte-length
License: MIT License: MIT
@ -2774,6 +2843,20 @@ Copyright: Copyright (c) 2019 Jordan Harband
Source: git+https://github.com/inspect-js/which-boxed-primitive.git Source: git+https://github.com/inspect-js/which-boxed-primitive.git
Link: https://github.com/inspect-js/which-boxed-primitive#readme Link: https://github.com/inspect-js/which-boxed-primitive#readme
----------- -----------
which-builtin-type
License: MIT
License File: node_modules/which-builtin-type/LICENSE
Copyright: Copyright (c) 2020 ECMAScript Shims
Source: git+https://github.com/inspect-js/which-builtin-type.git
Link: https://github.com/inspect-js/which-builtin-type#readme
-----------
which-collection
License: MIT
License File: node_modules/which-collection/LICENSE
Copyright: Copyright (c) 2019 Inspect JS
Source: git+https://github.com/inspect-js/which-collection.git
Link: https://github.com/inspect-js/which-collection#readme
-----------
which-module which-module
License: ISC License: ISC
License File: node_modules/which-module/LICENSE License File: node_modules/which-module/LICENSE
@ -2949,13 +3032,6 @@ Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Source: https://github.com/babel/babel.git Source: https://github.com/babel/babel.git
Link: https://babel.dev/docs/en/next/babel-helper-plugin-utils Link: https://babel.dev/docs/en/next/babel-helper-plugin-utils
----------- -----------
@babel/helper-simple-access
License: MIT
License File: node_modules/@babel/helper-simple-access/LICENSE
Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Source: https://github.com/babel/babel.git
Link: https://babel.dev/docs/en/next/babel-helper-simple-access
-----------
@babel/helper-string-parser @babel/helper-string-parser
License: MIT License: MIT
License File: node_modules/@babel/helper-string-parser/LICENSE License File: node_modules/@babel/helper-string-parser/LICENSE
@ -2984,13 +3060,6 @@ Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Source: https://github.com/babel/babel.git Source: https://github.com/babel/babel.git
Link: https://babel.dev/docs/en/next/babel-helpers Link: https://babel.dev/docs/en/next/babel-helpers
----------- -----------
@babel/highlight
License: MIT
License File: node_modules/@babel/highlight/LICENSE
Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Source: https://github.com/babel/babel.git
Link: https://babel.dev/docs/en/next/babel-highlight
-----------
@babel/parser @babel/parser
License: MIT License: MIT
License File: node_modules/@babel/parser/LICENSE License File: node_modules/@babel/parser/LICENSE
@ -3282,6 +3351,18 @@ Copyright: Copyright (c) 2018 Toru Nagashima
Source: https://github.com/eslint-community/regexpp Source: https://github.com/eslint-community/regexpp
Link: https://github.com/eslint-community/regexpp#readme Link: https://github.com/eslint-community/regexpp#readme
----------- -----------
@eslint/config-array
License: Apache-2.0
License File: node_modules/@eslint/config-array/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite#readme
-----------
@eslint/core
License: Apache-2.0
License File: node_modules/@eslint/core/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite#readme
-----------
@eslint/eslintrc @eslint/eslintrc
License: MIT License: MIT
License File: node_modules/@eslint/eslintrc/LICENSE License File: node_modules/@eslint/eslintrc/LICENSE
@ -3294,11 +3375,29 @@ License File: node_modules/@eslint/js/LICENSE
Source: https://github.com/eslint/eslint.git Source: https://github.com/eslint/eslint.git
Link: https://eslint.org Link: https://eslint.org
----------- -----------
@humanwhocodes/config-array @eslint/object-schema
License: Apache-2.0 License: Apache-2.0
License File: node_modules/@humanwhocodes/config-array/LICENSE License File: node_modules/@eslint/object-schema/LICENSE
Source: git+https://github.com/humanwhocodes/config-array.git Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/humanwhocodes/config-array#readme Link: https://github.com/eslint/rewrite#readme
-----------
@eslint/plugin-kit
License: Apache-2.0
License File: node_modules/@eslint/plugin-kit/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite#readme
-----------
@humanfs/core
License: Apache-2.0
License File: node_modules/@humanfs/core/LICENSE
Source: git+https://github.com/humanwhocodes/humanfs.git
Link: https://github.com/humanwhocodes/humanfs#readme
-----------
@humanfs/node
License: Apache-2.0
License File: node_modules/@humanfs/node/LICENSE
Source: git+https://github.com/humanwhocodes/humanfs.git
Link: https://github.com/humanwhocodes/humanfs#readme
----------- -----------
@humanwhocodes/module-importer @humanwhocodes/module-importer
License: Apache-2.0 License: Apache-2.0
@ -3306,13 +3405,11 @@ License File: node_modules/@humanwhocodes/module-importer/LICENSE
Source: git+https://github.com/humanwhocodes/module-importer.git Source: git+https://github.com/humanwhocodes/module-importer.git
Link: git+https://github.com/humanwhocodes/module-importer.git Link: git+https://github.com/humanwhocodes/module-importer.git
----------- -----------
@humanwhocodes/object-schema @humanwhocodes/retry
License: BSD-3-Clause License: Apache-2.0
License File: node_modules/@humanwhocodes/object-schema/LICENSE License File: node_modules/@humanwhocodes/retry/LICENSE
Copyright: Copyright (c) 2019, Human Who Codes Source: git+https://github.com/humanwhocodes/retry.git
All rights reserved. Link: git+https://github.com/humanwhocodes/retry.git
Source: git+https://github.com/humanwhocodes/object-schema.git
Link: https://github.com/humanwhocodes/object-schema#readme
----------- -----------
@istanbuljs/load-nyc-config @istanbuljs/load-nyc-config
License: ISC License: ISC
@ -3538,32 +3635,20 @@ Copyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and
Source: https://github.com/lezer-parser/xml.git Source: https://github.com/lezer-parser/xml.git
Link: https://github.com/lezer-parser/xml.git Link: https://github.com/lezer-parser/xml.git
----------- -----------
@marijn/buildtool
License: MIT
License File: node_modules/@marijn/buildtool/LICENSE
Copyright: Copyright (C) 2022 by Marijn Haverbeke <******@*********.******> and others
Source: https://github.com/marijnh/buildtool.git
Link: https://github.com/marijnh/buildtool.git
-----------
@marijn/find-cluster-break @marijn/find-cluster-break
License: MIT License: MIT
License File: node_modules/@marijn/find-cluster-break/LICENSE
Copyright: Copyright (C) 2024 by Marijn Haverbeke <******@*********.******>
Source: git+https://github.com/marijnh/find-cluster-break.git Source: git+https://github.com/marijnh/find-cluster-break.git
Link: https://github.com/marijnh/find-cluster-break#readme Link: https://github.com/marijnh/find-cluster-break#readme
----------- -----------
@nodelib/fs.scandir
License: MIT
License File: node_modules/@nodelib/fs.scandir/LICENSE
Copyright: Copyright (c) Denis Malinochkin
Source: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.scandir
Link: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.scandir
-----------
@nodelib/fs.stat
License: MIT
License File: node_modules/@nodelib/fs.stat/LICENSE
Copyright: Copyright (c) Denis Malinochkin
Source: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.stat
Link: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.stat
-----------
@nodelib/fs.walk
License: MIT
License File: node_modules/@nodelib/fs.walk/LICENSE
Copyright: Copyright (c) Denis Malinochkin
Source: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.walk
Link: https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.walk
-----------
@parcel/watcher-linux-x64-glibc @parcel/watcher-linux-x64-glibc
License: MIT License: MIT
License File: node_modules/@parcel/watcher-linux-x64-glibc/LICENSE License File: node_modules/@parcel/watcher-linux-x64-glibc/LICENSE
@ -3571,13 +3656,6 @@ 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
@ -3686,6 +3764,13 @@ Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__traverse Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__traverse
----------- -----------
@types/estree
License: MIT
License File: node_modules/@types/estree/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/estree
-----------
@types/graceful-fs @types/graceful-fs
License: MIT License: MIT
License File: node_modules/@types/graceful-fs/LICENSE License File: node_modules/@types/graceful-fs/LICENSE
@ -3728,11 +3813,25 @@ Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jsdom Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jsdom
----------- -----------
@types/json-schema
License: MIT
License File: node_modules/@types/json-schema/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema
-----------
@types/json5 @types/json5
License: MIT License: MIT
Source: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git Link: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git
----------- -----------
@types/mocha
License: MIT
License File: node_modules/@types/mocha/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mocha
-----------
@types/node @types/node
License: MIT License: MIT
License File: node_modules/@types/node/LICENSE License File: node_modules/@types/node/LICENSE
@ -3740,6 +3839,13 @@ Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node
----------- -----------
@types/sortablejs
License: MIT
License File: node_modules/@types/sortablejs/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/sortablejs
-----------
@types/stack-utils @types/stack-utils
License: MIT License: MIT
License File: node_modules/@types/stack-utils/LICENSE License File: node_modules/@types/stack-utils/LICENSE
@ -3767,10 +3873,3 @@ License File: node_modules/@types/yargs/LICENSE
Copyright: Copyright (c) Microsoft Corporation. Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/yargs Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/yargs
-----------
@ungap/structured-clone
License: ISC
License File: node_modules/@ungap/structured-clone/LICENSE
Copyright: Copyright (c) 2021, Andrea Giammarchi, @WebReflection
Source: git+https://github.com/ungap/structured-clone.git
Link: https://github.com/ungap/structured-clone#readme

View file

@ -47,34 +47,6 @@ Copyright: Copyright (c) 2012 Dragonfly Development Inc.
Source: https://github.com/dflydev/dflydev-dot-access-data.git Source: https://github.com/dflydev/dflydev-dot-access-data.git
Link: https://github.com/dflydev/dflydev-dot-access-data Link: https://github.com/dflydev/dflydev-dot-access-data
----------- -----------
doctrine/cache
License: MIT
License File: vendor/doctrine/cache/LICENSE
Copyright: Copyright (c) 2006-2015 Doctrine Project
Source: https://github.com/doctrine/cache.git
Link: https://www.doctrine-project.org/projects/cache.html
-----------
doctrine/dbal
License: MIT
License File: vendor/doctrine/dbal/LICENSE
Copyright: Copyright (c) 2006-2018 Doctrine Project
Source: https://github.com/doctrine/dbal.git
Link: https://www.doctrine-project.org/projects/dbal.html
-----------
doctrine/deprecations
License: MIT
License File: vendor/doctrine/deprecations/LICENSE
Copyright: Copyright (c) 2020-2021 Doctrine Project
Source: https://github.com/doctrine/deprecations.git
Link: https://www.doctrine-project.org/
-----------
doctrine/event-manager
License: MIT
License File: vendor/doctrine/event-manager/LICENSE
Copyright: Copyright (c) 2006-2015 Doctrine Project
Source: https://github.com/doctrine/event-manager.git
Link: https://www.doctrine-project.org/projects/event-manager.html
-----------
doctrine/inflector doctrine/inflector
License: MIT License: MIT
License File: vendor/doctrine/inflector/LICENSE License File: vendor/doctrine/inflector/LICENSE
@ -195,7 +167,7 @@ Link: https://github.com/guzzle/uri-template.git
intervention/gif intervention/gif
License: MIT License: MIT
License File: vendor/intervention/gif/LICENSE License File: vendor/intervention/gif/LICENSE
Copyright: Copyright (c) 2020-2024 Oliver Vogel Copyright: Copyright (c) 2020-present Oliver Vogel
Source: https://github.com/Intervention/gif.git Source: https://github.com/Intervention/gif.git
Link: https://github.com/intervention/gif Link: https://github.com/intervention/gif
----------- -----------
@ -311,6 +283,20 @@ Copyright: Copyright (c) 2013-2023 Alex Bilbie <*****@**********.***>
Source: https://github.com/thephpleague/oauth2-client.git Source: https://github.com/thephpleague/oauth2-client.git
Link: https://github.com/thephpleague/oauth2-client.git Link: https://github.com/thephpleague/oauth2-client.git
----------- -----------
league/uri
License: MIT
License File: vendor/league/uri/LICENSE
Copyright: Copyright (c) 2015 ignace nyamagana butera
Source: https://github.com/thephpleague/uri.git
Link: https://uri.thephpleague.com
-----------
league/uri-interfaces
License: MIT
License File: vendor/league/uri-interfaces/LICENSE
Copyright: Copyright (c) 2015 ignace nyamagana butera
Source: https://github.com/thephpleague/uri-interfaces.git
Link: https://uri.thephpleague.com
-----------
masterminds/html5 masterminds/html5
License: MIT License: MIT
License File: vendor/masterminds/html5/LICENSE.txt License File: vendor/masterminds/html5/LICENSE.txt
@ -336,7 +322,7 @@ nesbot/carbon
License: MIT License: MIT
License File: vendor/nesbot/carbon/LICENSE License File: vendor/nesbot/carbon/LICENSE
Copyright: Copyright (C) Brian Nesbitt Copyright: Copyright (C) Brian Nesbitt
Source: https://github.com/briannesbitt/Carbon.git Source: https://github.com/CarbonPHP/carbon.git
Link: https://carbon.nesbot.com Link: https://carbon.nesbot.com
----------- -----------
nette/schema nette/schema
@ -419,13 +405,6 @@ Copyright (c) 2021-2024 Till Krüss (modified work)
Source: https://github.com/predis/predis.git Source: https://github.com/predis/predis.git
Link: http://github.com/predis/predis Link: http://github.com/predis/predis
----------- -----------
psr/cache
License: MIT
License File: vendor/psr/cache/LICENSE.txt
Copyright: Copyright (c) 2015 PHP Framework Interoperability Group
Source: https://github.com/php-fig/cache.git
Link: https://github.com/php-fig/cache.git
-----------
psr/clock psr/clock
License: MIT License: MIT
License File: vendor/psr/clock/LICENSE License File: vendor/psr/clock/LICENSE
@ -571,6 +550,13 @@ Copyright: Copyright (c) 2019-present Fabien Potencier
Source: https://github.com/ssddanbrown/symfony-mailer.git Source: https://github.com/ssddanbrown/symfony-mailer.git
Link: https://symfony.com Link: https://symfony.com
----------- -----------
symfony/clock
License: MIT
License File: vendor/symfony/clock/LICENSE
Copyright: Copyright (c) 2022-present Fabien Potencier
Source: https://github.com/symfony/clock.git
Link: https://symfony.com
-----------
symfony/console symfony/console
License: MIT License: MIT
License File: vendor/symfony/console/LICENSE License File: vendor/symfony/console/LICENSE

64
eslint.config.mjs Normal file
View file

@ -0,0 +1,64 @@
import globals from 'globals';
import js from '@eslint/js';
export default [
js.configs.recommended,
{
ignores: ['resources/**/*-stub.js', 'resources/**/*.ts'],
}, {
languageOptions: {
globals: {
...globals.browser,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
indent: ['error', 4],
'arrow-parens': ['error', 'as-needed'],
'padded-blocks': ['error', {
blocks: 'never',
classes: 'always',
}],
'object-curly-spacing': ['error', 'never'],
'space-before-function-paren': ['error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
}],
'import/prefer-default-export': 'off',
'no-plusplus': ['error', {
allowForLoopAfterthoughts: true,
}],
'arrow-body-style': 'off',
'no-restricted-syntax': 'off',
'no-continue': 'off',
'prefer-destructuring': 'off',
'class-methods-use-this': 'off',
'no-param-reassign': 'off',
'no-console': ['warn', {
allow: ['error', 'warn'],
}],
'no-new': 'off',
'max-len': ['error', {
code: 110,
tabWidth: 4,
ignoreUrls: true,
ignoreComments: false,
ignoreRegExpLiterals: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
}],
},
}];

View file

@ -127,6 +127,14 @@ return [
'comment_update' => 'تعليق محدث', 'comment_update' => 'تعليق محدث',
'comment_delete' => 'تعليق محذوف', 'comment_delete' => 'تعليق محذوف',
// Sort Rules
'sort_rule_create' => 'created sort rule',
'sort_rule_create_notification' => 'Sort rule successfully created',
'sort_rule_update' => 'updated sort rule',
'sort_rule_update_notification' => 'Sort rule successfully updated',
'sort_rule_delete' => 'deleted sort rule',
'sort_rule_delete_notification' => 'Sort rule successfully deleted',
// Other // Other
'permissions_update' => 'تحديث الأذونات', 'permissions_update' => 'تحديث الأذونات',
]; ];

View file

@ -13,6 +13,7 @@ return [
'cancel' => 'إلغاء', 'cancel' => 'إلغاء',
'save' => 'حفظ', 'save' => 'حفظ',
'close' => 'إغلاق', 'close' => 'إغلاق',
'apply' => 'Apply',
'undo' => 'تراجع', 'undo' => 'تراجع',
'redo' => 'إعادة التنفيذ', 'redo' => 'إعادة التنفيذ',
'left' => 'يسار', 'left' => 'يسار',
@ -147,6 +148,7 @@ return [
'url' => 'URL', 'url' => 'URL',
'text_to_display' => 'Text to display', 'text_to_display' => 'Text to display',
'title' => 'Title', 'title' => 'Title',
'browse_links' => 'Browse links',
'open_link' => 'Open link', 'open_link' => 'Open link',
'open_link_in' => 'Open link in...', 'open_link_in' => 'Open link in...',
'open_link_current' => 'Current window', 'open_link_current' => 'Current window',

View file

@ -166,7 +166,9 @@ return [
'books_search_this' => 'البحث في هذا الكتاب', 'books_search_this' => 'البحث في هذا الكتاب',
'books_navigation' => 'تصفح الكتاب', 'books_navigation' => 'تصفح الكتاب',
'books_sort' => 'فرز محتويات الكتاب', 'books_sort' => 'فرز محتويات الكتاب',
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'فرز كتاب :bookName', 'books_sort_named' => 'فرز كتاب :bookName',
'books_sort_name' => 'ترتيب حسب الإسم', 'books_sort_name' => 'ترتيب حسب الإسم',
'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء', 'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء',

View file

@ -74,6 +74,36 @@ return [
'reg_confirm_restrict_domain_desc' => 'أدخل قائمة مفصولة بفواصل لنطاقات البريد الإلكتروني التي ترغب في تقييد التسجيل إليها. سيتم إرسال بريد إلكتروني للمستخدمين لتأكيد عنوانهم قبل السماح لهم بالتفاعل مع التطبيق. <br> لاحظ أن المستخدمين سيكونون قادرين على تغيير عناوين البريد الإلكتروني الخاصة بهم بعد التسجيل بنجاح.', 'reg_confirm_restrict_domain_desc' => 'أدخل قائمة مفصولة بفواصل لنطاقات البريد الإلكتروني التي ترغب في تقييد التسجيل إليها. سيتم إرسال بريد إلكتروني للمستخدمين لتأكيد عنوانهم قبل السماح لهم بالتفاعل مع التطبيق. <br> لاحظ أن المستخدمين سيكونون قادرين على تغيير عناوين البريد الإلكتروني الخاصة بهم بعد التسجيل بنجاح.',
'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود', 'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود',
// Sorting Settings
'sorting' => 'Sorting',
'sorting_book_default' => 'Default Book Sort',
'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
'sorting_rules' => 'Sort Rules',
'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_rule_create' => 'Create Sort Rule',
'sort_rule_edit' => 'Edit Sort Rule',
'sort_rule_delete' => 'Delete Sort Rule',
'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',
'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',
'sort_rule_details' => 'Sort Rule Details',
'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_rule_operations' => 'Sort Operations',
'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',
'sort_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Name - Alphabetical',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Chapters First',
'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings // Maintenance settings
'maint' => 'الصيانة', 'maint' => 'الصيانة',
'maint_image_cleanup' => 'تنظيف الصور', 'maint_image_cleanup' => 'تنظيف الصور',

View file

@ -15,7 +15,7 @@ return [
'page_restore' => 'възстановена страница', 'page_restore' => 'възстановена страница',
'page_restore_notification' => 'Страницата е възстановена успешно', 'page_restore_notification' => 'Страницата е възстановена успешно',
'page_move' => 'преместена страница', 'page_move' => 'преместена страница',
'page_move_notification' => 'Page successfully moved', 'page_move_notification' => 'Страницата беше успешно преместена',
// Chapters // Chapters
'chapter_create' => 'създадена глава', 'chapter_create' => 'създадена глава',
@ -25,13 +25,13 @@ return [
'chapter_delete' => 'изтрита глава', 'chapter_delete' => 'изтрита глава',
'chapter_delete_notification' => 'Успешно изтрита глава', 'chapter_delete_notification' => 'Успешно изтрита глава',
'chapter_move' => 'преместена глава', 'chapter_move' => 'преместена глава',
'chapter_move_notification' => 'Chapter successfully moved', 'chapter_move_notification' => 'Главата е успешно преместена',
// Books // Books
'book_create' => 'създадена книга', 'book_create' => 'създадена книга',
'book_create_notification' => 'Книгата е създадена успешно', 'book_create_notification' => 'Книгата е създадена успешно',
'book_create_from_chapter' => 'превърната глава в книга', 'book_create_from_chapter' => 'превърната глава в книга',
'book_create_from_chapter_notification' => 'Chapter successfully converted to a book', 'book_create_from_chapter_notification' => 'Главата е успешно преобразувана в книга',
'book_update' => 'обновена книга', 'book_update' => 'обновена книга',
'book_update_notification' => 'Книгата е обновена успешно', 'book_update_notification' => 'Книгата е обновена успешно',
'book_delete' => 'изтрита книга', 'book_delete' => 'изтрита книга',
@ -127,6 +127,14 @@ return [
'comment_update' => 'updated comment', 'comment_update' => 'updated comment',
'comment_delete' => 'deleted comment', 'comment_delete' => 'deleted comment',
// Sort Rules
'sort_rule_create' => 'created sort rule',
'sort_rule_create_notification' => 'Sort rule successfully created',
'sort_rule_update' => 'updated sort rule',
'sort_rule_update_notification' => 'Sort rule successfully updated',
'sort_rule_delete' => 'deleted sort rule',
'sort_rule_delete_notification' => 'Sort rule successfully deleted',
// Other // Other
'permissions_update' => 'обновени права', 'permissions_update' => 'обновени права',
]; ];

View file

@ -13,6 +13,7 @@ return [
'cancel' => 'Отказ', 'cancel' => 'Отказ',
'save' => 'Запис', 'save' => 'Запис',
'close' => 'Затваряне', 'close' => 'Затваряне',
'apply' => 'Apply',
'undo' => 'Отмяна', 'undo' => 'Отмяна',
'redo' => 'Повтаряне', 'redo' => 'Повтаряне',
'left' => 'Вляво', 'left' => 'Вляво',
@ -147,6 +148,7 @@ return [
'url' => 'URL', 'url' => 'URL',
'text_to_display' => 'Текст за показване', 'text_to_display' => 'Текст за показване',
'title' => 'Заглавие', 'title' => 'Заглавие',
'browse_links' => 'Browse links',
'open_link' => 'Open link', 'open_link' => 'Open link',
'open_link_in' => 'Open link in...', 'open_link_in' => 'Open link in...',
'open_link_current' => 'Текущ прозорец', 'open_link_current' => 'Текущ прозорец',

View file

@ -166,7 +166,9 @@ return [
'books_search_this' => 'Търси в книгата', 'books_search_this' => 'Търси в книгата',
'books_navigation' => 'Навигация на книгата', 'books_navigation' => 'Навигация на книгата',
'books_sort' => 'Сортирай съдържанието на книгата', 'books_sort' => 'Сортирай съдържанието на книгата',
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'Сортирай книга :bookName', 'books_sort_named' => 'Сортирай книга :bookName',
'books_sort_name' => 'Сортиране по име', 'books_sort_name' => 'Сортиране по име',
'books_sort_created' => 'Сортирай по дата на създаване', 'books_sort_created' => 'Сортирай по дата на създаване',

View file

@ -74,6 +74,36 @@ return [
'reg_confirm_restrict_domain_desc' => 'Въведи разделен със запетаи списък от имейл домейни, до които да бъде ограничена регистрацията. На потребителите ще им бъде изпратен имейл, за да потвърдят адреса, преди да могат да използват приложението. <br> Имай предвид, че потребителите ще могат да сменят имейл адресите си след успешна регистрация.', 'reg_confirm_restrict_domain_desc' => 'Въведи разделен със запетаи списък от имейл домейни, до които да бъде ограничена регистрацията. На потребителите ще им бъде изпратен имейл, за да потвърдят адреса, преди да могат да използват приложението. <br> Имай предвид, че потребителите ще могат да сменят имейл адресите си след успешна регистрация.',
'reg_confirm_restrict_domain_placeholder' => 'Няма наложени ограничения', 'reg_confirm_restrict_domain_placeholder' => 'Няма наложени ограничения',
// Sorting Settings
'sorting' => 'Sorting',
'sorting_book_default' => 'Default Book Sort',
'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
'sorting_rules' => 'Sort Rules',
'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_rule_create' => 'Create Sort Rule',
'sort_rule_edit' => 'Edit Sort Rule',
'sort_rule_delete' => 'Delete Sort Rule',
'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',
'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',
'sort_rule_details' => 'Sort Rule Details',
'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_rule_operations' => 'Sort Operations',
'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',
'sort_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Name - Alphabetical',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Chapters First',
'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings // Maintenance settings
'maint' => 'Поддръжка', 'maint' => 'Поддръжка',
'maint_image_cleanup' => 'Разчисти изображения', 'maint_image_cleanup' => 'Разчисти изображения',

View file

@ -127,6 +127,14 @@ return [
'comment_update' => 'মন্তব্য হালনাগাদ করেছেন', 'comment_update' => 'মন্তব্য হালনাগাদ করেছেন',
'comment_delete' => 'মন্তব্য মুছে ফেলেছেন', 'comment_delete' => 'মন্তব্য মুছে ফেলেছেন',
// Sort Rules
'sort_rule_create' => 'created sort rule',
'sort_rule_create_notification' => 'Sort rule successfully created',
'sort_rule_update' => 'updated sort rule',
'sort_rule_update_notification' => 'Sort rule successfully updated',
'sort_rule_delete' => 'deleted sort rule',
'sort_rule_delete_notification' => 'Sort rule successfully deleted',
// Other // Other
'permissions_update' => 'অনুমতিক্রম হালনাগাদ করেছেন', 'permissions_update' => 'অনুমতিক্রম হালনাগাদ করেছেন',
]; ];

View file

@ -14,16 +14,16 @@ return [
'log_in' => 'লগ ইন করুন', 'log_in' => 'লগ ইন করুন',
'log_in_with' => ':socialDriver দ্বারা লগইন করুন', 'log_in_with' => ':socialDriver দ্বারা লগইন করুন',
'sign_up_with' => ':socialDriver দ্বারা নিবন্ধিত হোন', 'sign_up_with' => ':socialDriver দ্বারা নিবন্ধিত হোন',
'logout' => 'Logout', 'logout' => 'লগআউট',
'name' => 'Name', 'name' => 'নাম',
'username' => 'Username', 'username' => 'ব্যবহারকারী',
'email' => 'Email', 'email' => 'ই-মেইল',
'password' => 'Password', 'password' => 'পাসওয়ার্ড',
'password_confirm' => 'Confirm Password', 'password_confirm' => 'পাসওয়ার্ডের পুনরাবৃত্তি',
'password_hint' => 'Must be at least 8 characters', 'password_hint' => 'ন্যূনতম ৮ অক্ষরের হতে হবে',
'forgot_password' => 'Forgot Password?', 'forgot_password' => 'পাসওয়ার্ড ভুলে গেছেন?',
'remember_me' => 'Remember Me', 'remember_me' => 'লগইন স্থায়িত্ব ধরে রাখুন',
'ldap_email_hint' => 'Please enter an email to use for this account.', 'ldap_email_hint' => 'Please enter an email to use for this account.',
'create_account' => 'Create Account', 'create_account' => 'Create Account',
'already_have_account' => 'Already have an account?', 'already_have_account' => 'Already have an account?',

View file

@ -13,6 +13,7 @@ return [
'cancel' => 'প্রত্যাহার করুন', 'cancel' => 'প্রত্যাহার করুন',
'save' => 'সংরক্ষণ করুন', 'save' => 'সংরক্ষণ করুন',
'close' => 'বন্ধ করুন', 'close' => 'বন্ধ করুন',
'apply' => 'Apply',
'undo' => 'প্রত্যাহার করুন', 'undo' => 'প্রত্যাহার করুন',
'redo' => 'পুনর্বহাল রাখুন', 'redo' => 'পুনর্বহাল রাখুন',
'left' => 'বাম', 'left' => 'বাম',
@ -147,6 +148,7 @@ return [
'url' => 'URL', 'url' => 'URL',
'text_to_display' => 'Text to display', 'text_to_display' => 'Text to display',
'title' => 'Title', 'title' => 'Title',
'browse_links' => 'Browse links',
'open_link' => 'Open link', 'open_link' => 'Open link',
'open_link_in' => 'Open link in...', 'open_link_in' => 'Open link in...',
'open_link_current' => 'Current window', 'open_link_current' => 'Current window',

View file

@ -166,7 +166,9 @@ return [
'books_search_this' => 'Search this book', 'books_search_this' => 'Search this book',
'books_navigation' => 'Book Navigation', 'books_navigation' => 'Book Navigation',
'books_sort' => 'Sort Book Contents', 'books_sort' => 'Sort Book Contents',
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'Sort Book :bookName', 'books_sort_named' => 'Sort Book :bookName',
'books_sort_name' => 'Sort by Name', 'books_sort_name' => 'Sort by Name',
'books_sort_created' => 'Sort by Created Date', 'books_sort_created' => 'Sort by Created Date',

View file

@ -74,6 +74,36 @@ return [
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.', 'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set', 'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
// Sorting Settings
'sorting' => 'Sorting',
'sorting_book_default' => 'Default Book Sort',
'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
'sorting_rules' => 'Sort Rules',
'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_rule_create' => 'Create Sort Rule',
'sort_rule_edit' => 'Edit Sort Rule',
'sort_rule_delete' => 'Delete Sort Rule',
'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',
'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',
'sort_rule_details' => 'Sort Rule Details',
'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_rule_operations' => 'Sort Operations',
'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',
'sort_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Name - Alphabetical',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Chapters First',
'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings // Maintenance settings
'maint' => 'Maintenance', 'maint' => 'Maintenance',
'maint_image_cleanup' => 'Cleanup Images', 'maint_image_cleanup' => 'Cleanup Images',

View file

@ -127,6 +127,14 @@ return [
'comment_update' => 'updated comment', 'comment_update' => 'updated comment',
'comment_delete' => 'deleted comment', 'comment_delete' => 'deleted comment',
// Sort Rules
'sort_rule_create' => 'created sort rule',
'sort_rule_create_notification' => 'Sort rule successfully created',
'sort_rule_update' => 'updated sort rule',
'sort_rule_update_notification' => 'Sort rule successfully updated',
'sort_rule_delete' => 'deleted sort rule',
'sort_rule_delete_notification' => 'Sort rule successfully deleted',
// Other // Other
'permissions_update' => 'je ažurirao/la dozvole', 'permissions_update' => 'je ažurirao/la dozvole',
]; ];

View file

@ -13,6 +13,7 @@ return [
'cancel' => 'Cancel', 'cancel' => 'Cancel',
'save' => 'Save', 'save' => 'Save',
'close' => 'Close', 'close' => 'Close',
'apply' => 'Apply',
'undo' => 'Undo', 'undo' => 'Undo',
'redo' => 'Redo', 'redo' => 'Redo',
'left' => 'Left', 'left' => 'Left',
@ -147,6 +148,7 @@ return [
'url' => 'URL', 'url' => 'URL',
'text_to_display' => 'Text to display', 'text_to_display' => 'Text to display',
'title' => 'Title', 'title' => 'Title',
'browse_links' => 'Browse links',
'open_link' => 'Open link', 'open_link' => 'Open link',
'open_link_in' => 'Open link in...', 'open_link_in' => 'Open link in...',
'open_link_current' => 'Current window', 'open_link_current' => 'Current window',

View file

@ -166,7 +166,9 @@ return [
'books_search_this' => 'Pretraži ovu knjigu', 'books_search_this' => 'Pretraži ovu knjigu',
'books_navigation' => 'Navigacija knjige', 'books_navigation' => 'Navigacija knjige',
'books_sort' => 'Sortiraj sadržaj knjige', 'books_sort' => 'Sortiraj sadržaj knjige',
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'Sortiraj knjigu :bookName', 'books_sort_named' => 'Sortiraj knjigu :bookName',
'books_sort_name' => 'Sortiraj po imenu', 'books_sort_name' => 'Sortiraj po imenu',
'books_sort_created' => 'Sortiraj po datumu kreiranja', 'books_sort_created' => 'Sortiraj po datumu kreiranja',

View file

@ -74,6 +74,36 @@ return [
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.', 'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set', 'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
// Sorting Settings
'sorting' => 'Sorting',
'sorting_book_default' => 'Default Book Sort',
'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
'sorting_rules' => 'Sort Rules',
'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_rule_create' => 'Create Sort Rule',
'sort_rule_edit' => 'Edit Sort Rule',
'sort_rule_delete' => 'Delete Sort Rule',
'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',
'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',
'sort_rule_details' => 'Sort Rule Details',
'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_rule_operations' => 'Sort Operations',
'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',
'sort_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Name - Alphabetical',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Chapters First',
'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings // Maintenance settings
'maint' => 'Maintenance', 'maint' => 'Maintenance',
'maint_image_cleanup' => 'Cleanup Images', 'maint_image_cleanup' => 'Cleanup Images',

View file

@ -127,6 +127,14 @@ return [
'comment_update' => 'ha actualitzat un comentari', 'comment_update' => 'ha actualitzat un comentari',
'comment_delete' => 'ha suprimit un comentari', 'comment_delete' => 'ha suprimit un comentari',
// Sort Rules
'sort_rule_create' => 'created sort rule',
'sort_rule_create_notification' => 'Sort rule successfully created',
'sort_rule_update' => 'updated sort rule',
'sort_rule_update_notification' => 'Sort rule successfully updated',
'sort_rule_delete' => 'deleted sort rule',
'sort_rule_delete_notification' => 'Sort rule successfully deleted',
// Other // Other
'permissions_update' => 'ha actualitzat els permisos', 'permissions_update' => 'ha actualitzat els permisos',
]; ];

View file

@ -13,6 +13,7 @@ return [
'cancel' => 'Cancel·la', 'cancel' => 'Cancel·la',
'save' => 'Desa', 'save' => 'Desa',
'close' => 'Tanca', 'close' => 'Tanca',
'apply' => 'Apply',
'undo' => 'Desfés', 'undo' => 'Desfés',
'redo' => 'Refés', 'redo' => 'Refés',
'left' => 'Esquerra', 'left' => 'Esquerra',
@ -147,6 +148,7 @@ return [
'url' => 'URL', 'url' => 'URL',
'text_to_display' => 'Text per a mostrar', 'text_to_display' => 'Text per a mostrar',
'title' => 'Títol', 'title' => 'Títol',
'browse_links' => 'Browse links',
'open_link' => 'Obre lenllaç', 'open_link' => 'Obre lenllaç',
'open_link_in' => 'Obre lenllaç…', 'open_link_in' => 'Obre lenllaç…',
'open_link_current' => 'A la finestra actual', 'open_link_current' => 'A la finestra actual',

View file

@ -166,7 +166,9 @@ return [
'books_search_this' => 'Cerca en aquest llibre', 'books_search_this' => 'Cerca en aquest llibre',
'books_navigation' => 'Navegació del llibre', 'books_navigation' => 'Navegació del llibre',
'books_sort' => 'Ordena el contingut dun llibre', 'books_sort' => 'Ordena el contingut dun llibre',
'books_sort_desc' => 'Moveu els capítols i les pàgines dun llibre per a reorganitzar-ne el contingut. Podeu afegir altres llibres perquè sigui més fàcil moure capítols i pàgines dun llibre a un altre.', 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_named' => 'Ordena el llibre &laquo;:bookName&raquo;', 'books_sort_named' => 'Ordena el llibre &laquo;:bookName&raquo;',
'books_sort_name' => 'Ordena pel nom', 'books_sort_name' => 'Ordena pel nom',
'books_sort_created' => 'Ordena per la data de creació', 'books_sort_created' => 'Ordena per la data de creació',

View file

@ -74,6 +74,36 @@ return [
'reg_confirm_restrict_domain_desc' => 'Introduïu una llista separada per comes dels dominis a què voleu restringir el registre. Senviarà un correu electrònic als usuaris perquè confirmin la seva adreça electrònica abans que puguin interactuar amb laplicació. <br> Tingueu en compte que els usuaris podran canviar ladreça electrònica un cop shagin registrat.', 'reg_confirm_restrict_domain_desc' => 'Introduïu una llista separada per comes dels dominis a què voleu restringir el registre. Senviarà un correu electrònic als usuaris perquè confirmin la seva adreça electrònica abans que puguin interactuar amb laplicació. <br> Tingueu en compte que els usuaris podran canviar ladreça electrònica un cop shagin registrat.',
'reg_confirm_restrict_domain_placeholder' => 'No hi ha cap restricció', 'reg_confirm_restrict_domain_placeholder' => 'No hi ha cap restricció',
// Sorting Settings
'sorting' => 'Sorting',
'sorting_book_default' => 'Default Book Sort',
'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.',
'sorting_rules' => 'Sort Rules',
'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.',
'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books',
'sort_rule_create' => 'Create Sort Rule',
'sort_rule_edit' => 'Edit Sort Rule',
'sort_rule_delete' => 'Delete Sort Rule',
'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.',
'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?',
'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?',
'sort_rule_details' => 'Sort Rule Details',
'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.',
'sort_rule_operations' => 'Sort Operations',
'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',
'sort_rule_available_operations' => 'Available Operations',
'sort_rule_available_operations_empty' => 'No operations remaining',
'sort_rule_configured_operations' => 'Configured Operations',
'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list',
'sort_rule_op_asc' => '(Asc)',
'sort_rule_op_desc' => '(Desc)',
'sort_rule_op_name' => 'Name - Alphabetical',
'sort_rule_op_name_numeric' => 'Name - Numeric',
'sort_rule_op_created_date' => 'Created Date',
'sort_rule_op_updated_date' => 'Updated Date',
'sort_rule_op_chapters_first' => 'Chapters First',
'sort_rule_op_chapters_last' => 'Chapters Last',
// Maintenance settings // Maintenance settings
'maint' => 'Manteniment', 'maint' => 'Manteniment',
'maint_image_cleanup' => 'Neteja dimatges', 'maint_image_cleanup' => 'Neteja dimatges',

View file

@ -127,6 +127,14 @@ return [
'comment_update' => 'aktualizoval komentář', 'comment_update' => 'aktualizoval komentář',
'comment_delete' => 'odstranil komentář', 'comment_delete' => 'odstranil komentář',
// Sort Rules
'sort_rule_create' => 'vytvořil/a pravidlo řazení',
'sort_rule_create_notification' => 'Pravidlo řazení bylo úspěšně vytvořeno',
'sort_rule_update' => 'aktualizoval/a pravidlo řazení',
'sort_rule_update_notification' => 'Pravidlo řazení bylo úspěšně aktualizováno',
'sort_rule_delete' => 'odstranil/a pravidlo řazení',
'sort_rule_delete_notification' => 'Pravidlo řazení bylo úspěšně odstraněno',
// Other // Other
'permissions_update' => 'oprávnění upravena', 'permissions_update' => 'oprávnění upravena',
]; ];

View file

@ -13,6 +13,7 @@ return [
'cancel' => 'Zrušit', 'cancel' => 'Zrušit',
'save' => 'Uložit', 'save' => 'Uložit',
'close' => 'Zavřít', 'close' => 'Zavřít',
'apply' => 'Použít',
'undo' => 'Zpět', 'undo' => 'Zpět',
'redo' => 'Znovu', 'redo' => 'Znovu',
'left' => 'Vlevo', 'left' => 'Vlevo',
@ -147,6 +148,7 @@ return [
'url' => 'Adresa URL', 'url' => 'Adresa URL',
'text_to_display' => 'Text k zobrazení', 'text_to_display' => 'Text k zobrazení',
'title' => 'Titulek', 'title' => 'Titulek',
'browse_links' => 'Procházet odkazy',
'open_link' => 'Otevřít odkaz', 'open_link' => 'Otevřít odkaz',
'open_link_in' => 'Otevřít odkaz v...', 'open_link_in' => 'Otevřít odkaz v...',
'open_link_current' => 'Aktuální okno', 'open_link_current' => 'Aktuální okno',

View file

@ -166,7 +166,9 @@ return [
'books_search_this' => 'Prohledat tuto knihu', 'books_search_this' => 'Prohledat tuto knihu',
'books_navigation' => 'Navigace knihy', 'books_navigation' => 'Navigace knihy',
'books_sort' => 'Seřadit obsah knihy', 'books_sort' => 'Seřadit obsah knihy',
'books_sort_desc' => 'Přesunout kapitoly a stránky v knize pro přeuspořádání obsahu. Mohou být přidány další knihy, které umožňují snadný přesun kapitol a stránek mezi knihami.', 'books_sort_desc' => 'Pro přeuspořádání obsahu přesuňte kapitoly a stránky v knize. Mohou být přidány další knihy, které umožní snadný přesun kapitol a stránek mezi knihami. Volitelně lze nastavit pravidlo automatického řazení, aby se při změnách automaticky seřadil obsah této knihy.',
'books_sort_auto_sort' => 'Možnost automatického řazení',
'books_sort_auto_sort_active' => 'Aktivní automatické řazení: :sortName',
'books_sort_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í',

View file

@ -74,6 +74,36 @@ return [
'reg_confirm_restrict_domain_desc' => 'Zadejte seznam e-mailových domén oddělených čárkami, na které chcete registraci omezit. Registrujícímu se uživateli bude zaslán e-mail, aby ověřil svoji e-mailovou adresu před tím, než mu bude přístup do aplikace povolen. <br> Upozorňujeme, že po úspěšné registraci může uživatel svoji e-mailovou adresu změnit.', 'reg_confirm_restrict_domain_desc' => 'Zadejte seznam e-mailových domén oddělených čárkami, na které chcete registraci omezit. Registrujícímu se uživateli bude zaslán e-mail, aby ověřil svoji e-mailovou adresu před tím, než mu bude přístup do aplikace povolen. <br> Upozorňujeme, že po úspěšné registraci může uživatel svoji e-mailovou adresu změnit.',
'reg_confirm_restrict_domain_placeholder' => 'Žádná omezení nebyla nastavena', 'reg_confirm_restrict_domain_placeholder' => 'Žádná omezení nebyla nastavena',
// Sorting Settings
'sorting' => 'Řazení',
'sorting_book_default' => 'Výchozí řazení knih',
'sorting_book_default_desc' => 'Vybere výchozí pravidlo řazení pro nové knihy. Řazení neovlivní existující knihy a může být upraveno u konkrétní knihy.',
'sorting_rules' => 'Pravidla řazení',
'sorting_rules_desc' => 'Toto jsou předem definovaná pravidla řazení, která mohou být použita na webu.',
'sort_rule_assigned_to_x_books' => 'Přiřazeno k :count knize|Přiřazeno :count knihám',
'sort_rule_create' => 'Vytvořit pravidlo řazení',
'sort_rule_edit' => 'Upravit pravidlo řazení',
'sort_rule_delete' => 'Odstranit pravidlo řazení',
'sort_rule_delete_desc' => 'Odstraní toto pravidlo řazení z webu. Knihy, které jej používají, se vrátí k ručnímu řazení.',
'sort_rule_delete_warn_books' => 'Toto pravidlo řazení se v současné době používá na :count knihách. Opravdu ho chcete smazat?',
'sort_rule_delete_warn_default' => 'Toto pravidlo řazení je v současné době používáno jako výchozí. Opravdu ho chcete odstranit?',
'sort_rule_details' => 'Podrobnosti pravidla pro řazení',
'sort_rule_details_desc' => 'Nastavte název pro toto pravidlo, který se zobrazí v seznamu při výběru řazení.',
'sort_rule_operations' => 'Možnosti řazení',
'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.',
'sort_rule_available_operations' => 'Dostupné operace',
'sort_rule_available_operations_empty' => 'Žádné zbývající operace',
'sort_rule_configured_operations' => 'Konfigurované operace',
'sort_rule_configured_operations_empty' => 'Přetáhněte/přidejte operace ze seznamu "Dostupné operace"',
'sort_rule_op_asc' => '(Vzest)',
'sort_rule_op_desc' => '(Sest)',
'sort_rule_op_name' => 'Název - abecední',
'sort_rule_op_name_numeric' => 'Název - číselné',
'sort_rule_op_created_date' => 'Datum vytvoření',
'sort_rule_op_updated_date' => 'Datum aktualizace',
'sort_rule_op_chapters_first' => 'Kapitoly jako první',
'sort_rule_op_chapters_last' => 'Kapitoly jako poslední',
// Maintenance settings // Maintenance settings
'maint' => 'Údržba', 'maint' => 'Údržba',
'maint_image_cleanup' => 'Pročistění obrázků', 'maint_image_cleanup' => 'Pročistění obrázků',

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