mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-15 11:19:37 +03:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
304ade418e | ||
|
|
997931c42f | ||
|
|
0ec0913846 | ||
|
|
e980564fd6 | ||
|
|
8a9215ecad | ||
|
|
304a1d8f91 | ||
|
|
dfbc78947f | ||
|
|
4f5ad171ac | ||
|
|
94b1cffa2d | ||
|
|
268e353431 | ||
|
|
b491b5fbca | ||
|
|
13dae24cbe | ||
|
|
6211d6bcfc | ||
|
|
a384599cfa | ||
|
|
dca14feaaa | ||
|
|
d7ccb3ce6a | ||
|
|
6548ea4a12 | ||
|
|
c3a1fabbf0 | ||
|
|
d2542d6265 | ||
|
|
0e343c408f | ||
|
|
5c78f8352e | ||
|
|
35b45a2b8d | ||
|
|
5050719ea3 | ||
|
|
5508c171db | ||
|
|
3b4d3430a5 | ||
|
|
213a86e3c0 | ||
|
|
2b746425c9 | ||
|
|
5c15f4add2 | ||
|
|
92ad81429f | ||
|
|
f1b8e857bf | ||
|
|
c291d27c19 | ||
|
|
f4449928f8 | ||
|
|
45a15b4792 | ||
|
|
2291d78382 | ||
|
|
7901ca9e6b | ||
|
|
a7de251876 | ||
|
|
7bd89316bc | ||
|
|
b9306a9029 | ||
|
|
a208c46b62 | ||
|
|
a65701294e | ||
|
|
69683d50ec | ||
|
|
37d020c083 | ||
|
|
ec79517493 | ||
|
|
d938565839 | ||
|
|
ccd94684eb | ||
|
|
103a8a8e8e | ||
|
|
c13ce18837 | ||
|
|
7093daa49d | ||
|
|
b897af2ed0 | ||
|
|
d28278bba6 | ||
|
|
12cc2f0689 | ||
|
|
bf8a84a8b1 | ||
|
|
4f5f7c10b1 | ||
|
|
a34023f715 | ||
|
|
b2ac3e0834 | ||
|
|
5b0cb3dd50 | ||
|
|
ac0cd9995d | ||
|
|
7e03a973d8 | ||
|
|
d89a2fdb15 | ||
|
|
958b537a49 | ||
|
|
8a66365d48 | ||
|
|
04cca77ae6 | ||
|
|
c091f67db3 | ||
|
|
7f5fd16dc6 | ||
|
|
0d1a237f81 | ||
|
|
786a434c03 | ||
|
|
25c4f4b02b | ||
|
|
481580be17 | ||
|
|
593645acfe | ||
|
|
b9751807e7 | ||
|
|
ee88832f1a | ||
|
|
dbda82ef92 | ||
|
|
ad8bc5fe21 | ||
|
|
5bf75786c6 | ||
|
|
cf9ccfcd5b | ||
|
|
5116d83d38 | ||
|
|
387c786768 | ||
|
|
2641586a6f | ||
|
|
33b46882f3 | ||
|
|
9a5c287470 | ||
|
|
6effc6d262 | ||
|
|
ff6c5aaecb | ||
|
|
1ff2826678 | ||
|
|
7e31725d48 | ||
|
|
6d7ff59a89 |
@@ -56,6 +56,7 @@ APP_PROXIES=null
|
||||
|
||||
# Database details
|
||||
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
|
||||
# An ipv6 address can be used via the square bracket format ([::1]).
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=database_database
|
||||
|
||||
17
.github/translators.txt
vendored
17
.github/translators.txt
vendored
@@ -461,3 +461,20 @@ Yannis Karlaftis (meliseus) :: Greek
|
||||
felixxx :: German Informal
|
||||
randi (randi65535) :: Korean
|
||||
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
|
||||
mabdullah :: Arabic
|
||||
Skrabák Csaba (kekcsi) :: Hungarian
|
||||
Evert Meulie (Evert) :: Norwegian Bokmal
|
||||
Jasper Backer (jasperb) :: Dutch
|
||||
Alexandar Cavdarovski (ace.200112) :: Swedish
|
||||
구닥다리TV (yjj8353) :: Korean
|
||||
Onur Oskay (o.oskay) :: Turkish
|
||||
Sébastien Merveille (SebastienMerv) :: French
|
||||
Maxim Kouznetsov (masya.work) :: Hebrew
|
||||
|
||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.1', '8.2', '8.3', '8.4']
|
||||
php: ['8.2', '8.3', '8.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
4
.github/workflows/test-php.yml
vendored
4
.github/workflows/test-php.yml
vendored
@@ -13,10 +13,10 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.1', '8.2', '8.3', '8.4']
|
||||
php: ['8.2', '8.3', '8.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,3 +32,4 @@ webpack-stats.json
|
||||
phpstan.neon
|
||||
esbuild-meta.json
|
||||
.phpactor.json
|
||||
/*.zip
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -8,27 +8,15 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ExternalBaseUserProvider implements UserProvider
|
||||
{
|
||||
/**
|
||||
* The user model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* LdapUserProvider constructor.
|
||||
*/
|
||||
public function __construct(string $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
public function __construct(
|
||||
protected string $model
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the model.
|
||||
*
|
||||
* @return Model
|
||||
*/
|
||||
public function createModel()
|
||||
public function createModel(): Model
|
||||
{
|
||||
$class = '\\' . ltrim($this->model, '\\');
|
||||
|
||||
@@ -37,12 +25,8 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -50,12 +34,9 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
/**
|
||||
* Retrieve a user by their unique identifier and "remember me" token.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @param string $token
|
||||
*
|
||||
* @return Authenticatable|null
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
public function retrieveByToken(mixed $identifier, $token): null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -75,12 +56,8 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
|
||||
/**
|
||||
* 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
|
||||
$model = $this->createModel();
|
||||
@@ -92,15 +69,15 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
|
||||
/**
|
||||
* 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.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class Ldap
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class Ldap
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class Ldap
|
||||
*
|
||||
* @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);
|
||||
|
||||
@@ -99,7 +99,7 @@ class Ldap
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
@@ -112,10 +112,14 @@ class LdapService
|
||||
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 = [
|
||||
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
|
||||
'name' => $this->getUserDisplayName($user, $displayNameAttrs, $userCn),
|
||||
'name' => $this->getUserDisplayName($user, $displayNameAttrs, $nameDefault),
|
||||
'dn' => $user['dn'],
|
||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
||||
'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
|
||||
|
||||
@@ -92,7 +92,7 @@ class SocialDriverManager
|
||||
string $driverName,
|
||||
array $config,
|
||||
string $socialiteHandler,
|
||||
callable $configureForRedirect = null
|
||||
?callable $configureForRedirect = null
|
||||
) {
|
||||
$this->validDrivers[] = $driverName;
|
||||
config()->set('services.' . $driverName, $config);
|
||||
|
||||
@@ -71,6 +71,10 @@ class ActivityType
|
||||
const IMPORT_RUN = 'import_run';
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Activity\Controllers;
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Activity\Models\Activity;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Sorting\SortUrl;
|
||||
use BookStack\Util\SimpleListOptions;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -65,6 +66,7 @@ class AuditLogController extends Controller
|
||||
'filters' => $filters,
|
||||
'listOptions' => $listOptions,
|
||||
'activityTypes' => $types,
|
||||
'filterSortUrl' => new SortUrl('settings/audit', array_filter($request->except('page')))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ class Comment extends Model implements Loggable
|
||||
use HasCreatorAndUpdater;
|
||||
|
||||
protected $fillable = ['parent_id'];
|
||||
protected $appends = ['created', 'updated'];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
|
||||
|
||||
@@ -42,4 +42,12 @@ class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the registration of Laravel's default email verification system
|
||||
*/
|
||||
protected function configureEmailVerification(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,5 +85,12 @@ class RouteServiceProvider extends ServiceProvider
|
||||
RateLimiter::for('public', function (Request $request) {
|
||||
return Limit::perMinute(10)->by($request->ip());
|
||||
});
|
||||
|
||||
RateLimiter::for('exports', function (Request $request) {
|
||||
$user = user();
|
||||
$attempts = $user->isGuest() ? 4 : 10;
|
||||
$key = $user->isGuest() ? $request->ip() : $user->id;
|
||||
return Limit::perMinute($attempts)->by($key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use BookStack\App\Model;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use BookStack\Settings\SettingService;
|
||||
use BookStack\Users\Models\User;
|
||||
@@ -42,9 +43,9 @@ function user(): User
|
||||
* Check if the current user has a permission. If an ownable element
|
||||
* is passed in the jointPermissions are checked against that particular item.
|
||||
*/
|
||||
function userCan(string $permission, Model $ownable = null): bool
|
||||
function userCan(string $permission, ?Model $ownable = null): bool
|
||||
{
|
||||
if ($ownable === null) {
|
||||
if (is_null($ownable)) {
|
||||
return user()->can($permission);
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@ function userCanOnAny(string $action, string $entityClass = ''): bool
|
||||
*
|
||||
* @return mixed|SettingService
|
||||
*/
|
||||
function setting(string $key = null, $default = null)
|
||||
function setting(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
$settingService = app()->make(SettingService::class);
|
||||
|
||||
@@ -88,43 +89,10 @@ function setting(string $key = null, $default = null)
|
||||
*/
|
||||
function theme_path(string $path = ''): ?string
|
||||
{
|
||||
$theme = config('view.theme');
|
||||
|
||||
$theme = Theme::getTheme();
|
||||
if (!$theme) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -35,10 +35,6 @@ return [
|
||||
// Available caches stores
|
||||
'stores' => [
|
||||
|
||||
'apc' => [
|
||||
'driver' => 'apc',
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
@@ -49,6 +45,7 @@ return [
|
||||
'table' => 'cache',
|
||||
'connection' => null,
|
||||
'lock_connection' => null,
|
||||
'lock_table' => null,
|
||||
],
|
||||
|
||||
'file' => [
|
||||
|
||||
@@ -40,12 +40,16 @@ if (env('REDIS_SERVERS', false)) {
|
||||
|
||||
// MYSQL
|
||||
// Split out port from host if set
|
||||
$mysql_host = env('DB_HOST', 'localhost');
|
||||
$mysql_host_exploded = explode(':', $mysql_host);
|
||||
$mysql_port = env('DB_PORT', 3306);
|
||||
if (count($mysql_host_exploded) > 1) {
|
||||
$mysql_host = $mysql_host_exploded[0];
|
||||
$mysql_port = intval($mysql_host_exploded[1]);
|
||||
$mysqlHost = env('DB_HOST', 'localhost');
|
||||
$mysqlHostExploded = explode(':', $mysqlHost);
|
||||
$mysqlPort = env('DB_PORT', 3306);
|
||||
$mysqlHostIpv6 = str_starts_with($mysqlHost, '[');
|
||||
if ($mysqlHostIpv6 && str_contains($mysqlHost, ']:')) {
|
||||
$mysqlHost = implode(':', array_slice($mysqlHostExploded, 0, -1));
|
||||
$mysqlPort = intval(end($mysqlHostExploded));
|
||||
} else if (!$mysqlHostIpv6 && count($mysqlHostExploded) > 1) {
|
||||
$mysqlHost = $mysqlHostExploded[0];
|
||||
$mysqlPort = intval($mysqlHostExploded[1]);
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -61,12 +65,12 @@ return [
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => $mysql_host,
|
||||
'host' => $mysqlHost,
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'port' => $mysql_port,
|
||||
'port' => $mysqlPort,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
// Prefixes are only semi-supported and may be unstable
|
||||
@@ -88,7 +92,7 @@ return [
|
||||
'database' => 'bookstack-test',
|
||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||
'port' => $mysql_port,
|
||||
'port' => $mysqlPort,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
|
||||
@@ -114,6 +114,7 @@ return [
|
||||
* @var array
|
||||
*/
|
||||
'allowed_protocols' => [
|
||||
"data://" => ["rules" => []],
|
||||
'file://' => ['rules' => []],
|
||||
'http://' => ['rules' => []],
|
||||
'https://' => ['rules' => []],
|
||||
|
||||
@@ -33,12 +33,14 @@ return [
|
||||
'driver' => 'local',
|
||||
'root' => public_path(),
|
||||
'visibility' => 'public',
|
||||
'serve' => false,
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
'local_secure_attachments' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('uploads/files/'),
|
||||
'serve' => false,
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
@@ -46,6 +48,7 @@ return [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('uploads/images/'),
|
||||
'visibility' => 'public',
|
||||
'serve' => false,
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'verify_peer' => env('MAIL_VERIFY_SSL', true),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN'),
|
||||
'local_domain' => null,
|
||||
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
|
||||
],
|
||||
|
||||
@@ -64,12 +64,4 @@ return [
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// Email markdown configuration
|
||||
'markdown' => [
|
||||
'theme' => 'default',
|
||||
'paths' => [
|
||||
resource_path('views/vendor/mail'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -23,6 +23,7 @@ return [
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => null,
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
|
||||
99
app/Console/Commands/AssignSortRuleCommand.php
Normal file
99
app/Console/Commands/AssignSortRuleCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class BookController extends Controller
|
||||
/**
|
||||
* 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');
|
||||
|
||||
@@ -93,7 +93,7 @@ class BookController extends Controller
|
||||
* @throws ImageUploadException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request, string $shelfSlug = null)
|
||||
public function store(Request $request, ?string $shelfSlug = null)
|
||||
{
|
||||
$this->checkPermission('book-create-all');
|
||||
$validated = $this->validate($request, [
|
||||
|
||||
@@ -41,7 +41,7 @@ class PageController extends Controller
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function create(string $bookSlug, string $chapterSlug = null)
|
||||
public function create(string $bookSlug, ?string $chapterSlug = null)
|
||||
{
|
||||
if ($chapterSlug) {
|
||||
$parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||
@@ -69,7 +69,7 @@ class PageController extends Controller
|
||||
*
|
||||
* @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, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
|
||||
@@ -43,7 +43,6 @@ class PageRevisionController extends Controller
|
||||
->selectRaw("IF(markdown = '', false, true) as is_markdown")
|
||||
->with(['page.book', 'createdBy'])
|
||||
->reorder('id', $listOptions->getOrder())
|
||||
->reorder('created_at', $listOptions->getOrder())
|
||||
->paginate(50);
|
||||
|
||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
|
||||
@@ -52,6 +51,7 @@ class PageRevisionController extends Controller
|
||||
'revisions' => $revisions,
|
||||
'page' => $page,
|
||||
'listOptions' => $listOptions,
|
||||
'oldestRevisionId' => $page->revisions()->min('id'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Sorting\SortRule;
|
||||
use BookStack\Uploads\Image;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -16,12 +17,14 @@ use Illuminate\Support\Collection;
|
||||
* @property string $description
|
||||
* @property int $image_id
|
||||
* @property ?int $default_template_id
|
||||
* @property ?int $sort_rule_id
|
||||
* @property Image|null $cover
|
||||
* @property \Illuminate\Database\Eloquent\Collection $chapters
|
||||
* @property \Illuminate\Database\Eloquent\Collection $pages
|
||||
* @property \Illuminate\Database\Eloquent\Collection $directPages
|
||||
* @property \Illuminate\Database\Eloquent\Collection $shelves
|
||||
* @property ?Page $defaultTemplate
|
||||
* @property ?SortRule $sortRule
|
||||
*/
|
||||
class Book extends Entity implements HasCoverImage
|
||||
{
|
||||
@@ -82,6 +85,14 @@ class Book extends Entity implements HasCoverImage
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type')
|
||||
@@ -26,7 +26,7 @@ class QueryPopular
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
if (!empty($filterModels)) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Activity\TagRepo;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\HasCoverImage;
|
||||
@@ -12,6 +13,7 @@ use BookStack\Entities\Queries\PageQueries;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\References\ReferenceStore;
|
||||
use BookStack\References\ReferenceUpdater;
|
||||
use BookStack\Sorting\BookSorter;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Util\HtmlDescriptionFilter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
@@ -24,6 +26,7 @@ class BaseRepo
|
||||
protected ReferenceUpdater $referenceUpdater,
|
||||
protected ReferenceStore $referenceStore,
|
||||
protected PageQueries $pageQueries,
|
||||
protected BookSorter $bookSorter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -134,6 +137,18 @@ class BaseRepo
|
||||
$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
|
||||
{
|
||||
if (!in_array(HasHtmlDescription::class, class_uses($entity))) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Sorting\SortRule;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Exception;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
@@ -33,6 +34,12 @@ class BookRepo
|
||||
$this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ class ChapterRepo
|
||||
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
|
||||
Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
|
||||
|
||||
$this->baseRepo->sortParent($chapter);
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
@@ -50,6 +52,8 @@ class ChapterRepo
|
||||
|
||||
Activity::add(ActivityType::CHAPTER_UPDATE, $chapter);
|
||||
|
||||
$this->baseRepo->sortParent($chapter);
|
||||
|
||||
return $chapter;
|
||||
}
|
||||
|
||||
@@ -88,6 +92,8 @@ class ChapterRepo
|
||||
$chapter->rebuildPermissions();
|
||||
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
|
||||
|
||||
$this->baseRepo->sortParent($chapter);
|
||||
|
||||
return $parent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ class PageRepo
|
||||
$draft->refresh();
|
||||
|
||||
Activity::add(ActivityType::PAGE_CREATE, $draft);
|
||||
$this->baseRepo->sortParent($draft);
|
||||
|
||||
return $draft;
|
||||
}
|
||||
@@ -128,6 +129,7 @@ class PageRepo
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::PAGE_UPDATE, $page);
|
||||
$this->baseRepo->sortParent($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
@@ -243,6 +245,8 @@ class PageRepo
|
||||
Activity::add(ActivityType::PAGE_RESTORE, $page);
|
||||
Activity::add(ActivityType::REVISION_RESTORE, $revision);
|
||||
|
||||
$this->baseRepo->sortParent($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -272,6 +276,8 @@ class PageRepo
|
||||
|
||||
Activity::add(ActivityType::PAGE_MOVE, $page);
|
||||
|
||||
$this->baseRepo->sortParent($page);
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class RevisionRepo
|
||||
/**
|
||||
* 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();
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Queries\EntityQueries;
|
||||
use BookStack\Sorting\BookSortMap;
|
||||
use BookStack\Sorting\BookSortMapItem;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BookContents
|
||||
@@ -103,211 +105,4 @@ class BookContents
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class BookExportController extends Controller
|
||||
protected ExportFormatter $exportFormatter,
|
||||
) {
|
||||
$this->middleware('can:content-export');
|
||||
$this->middleware('throttle:exports');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +76,6 @@ class BookExportController extends Controller
|
||||
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
|
||||
$zip = $builder->buildForBook($book);
|
||||
|
||||
return $this->download()->streamedDirectly(fopen($zip, 'r'), $bookSlug . '.zip', filesize($zip));
|
||||
return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class ChapterExportController extends Controller
|
||||
protected ExportFormatter $exportFormatter,
|
||||
) {
|
||||
$this->middleware('can:content-export');
|
||||
$this->middleware('throttle:exports');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,6 +82,6 @@ class ChapterExportController extends Controller
|
||||
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
|
||||
$zip = $builder->buildForChapter($chapter);
|
||||
|
||||
return $this->download()->streamedDirectly(fopen($zip, 'r'), $chapterSlug . '.zip', filesize($zip));
|
||||
return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ class PageExportController extends Controller
|
||||
protected ExportFormatter $exportFormatter,
|
||||
) {
|
||||
$this->middleware('can:content-export');
|
||||
$this->middleware('throttle:exports');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,6 +86,6 @@ class PageExportController extends Controller
|
||||
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
|
||||
$zip = $builder->buildForPage($page);
|
||||
|
||||
return $this->download()->streamedDirectly(fopen($zip, 'r'), $pageSlug . '.zip', filesize($zip));
|
||||
return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,18 +90,28 @@ class PdfGenerator
|
||||
$process = Process::fromShellCommandline($command);
|
||||
$process->setTimeout($timeout);
|
||||
|
||||
$cleanup = function () use ($inputHtml, $outputPdf) {
|
||||
foreach ([$inputHtml, $outputPdf] as $file) {
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
$process->run();
|
||||
} catch (ProcessTimedOutException $e) {
|
||||
$cleanup();
|
||||
throw new PdfExportException("PDF Export via command failed due to timeout at {$timeout} second(s)");
|
||||
}
|
||||
|
||||
if (!$process->isSuccessful()) {
|
||||
$cleanup();
|
||||
throw new PdfExportException("PDF Export via command failed with exit code {$process->getExitCode()}, stdout: {$process->getOutput()}, stderr: {$process->getErrorOutput()}");
|
||||
}
|
||||
|
||||
$pdfContents = file_get_contents($outputPdf);
|
||||
unlink($outputPdf);
|
||||
$cleanup();
|
||||
|
||||
if ($pdfContents === false) {
|
||||
throw new PdfExportException("PDF Export via command failed, unable to read PDF output file");
|
||||
|
||||
@@ -84,10 +84,27 @@ class ZipExportBuilder
|
||||
$zip->addEmptyDir('files');
|
||||
|
||||
$toRemove = [];
|
||||
$this->files->extractEach(function ($filePath, $fileRef) use ($zip, &$toRemove) {
|
||||
$zip->addFile($filePath, "files/$fileRef");
|
||||
$toRemove[] = $filePath;
|
||||
});
|
||||
$addedNames = [];
|
||||
|
||||
try {
|
||||
$this->files->extractEach(function ($filePath, $fileRef) use ($zip, &$toRemove, &$addedNames) {
|
||||
$entryName = "files/$fileRef";
|
||||
$zip->addFile($filePath, $entryName);
|
||||
$toRemove[] = $filePath;
|
||||
$addedNames[] = $entryName;
|
||||
});
|
||||
} catch (\Exception $exception) {
|
||||
// Cleanup the files we've processed so far and respond back with error
|
||||
foreach ($toRemove as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
foreach ($addedNames as $name) {
|
||||
$zip->deleteName($name);
|
||||
}
|
||||
$zip->close();
|
||||
unlink($zipFile);
|
||||
throw new ZipExportException("Failed to add files for ZIP export, received error: " . $exception->getMessage());
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
class DownloadResponseFactory
|
||||
{
|
||||
public function __construct(
|
||||
protected Request $request
|
||||
protected Request $request,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -35,6 +35,33 @@ class DownloadResponseFactory
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that downloads the given file via a stream.
|
||||
* Has the option to delete the provided file once the stream is closed.
|
||||
*/
|
||||
public function streamedFileDirectly(string $filePath, string $fileName, bool $deleteAfter = false): StreamedResponse
|
||||
{
|
||||
$fileSize = filesize($filePath);
|
||||
$stream = fopen($filePath, 'r');
|
||||
|
||||
if ($deleteAfter) {
|
||||
// Delete the given file if it still exists after the app terminates
|
||||
$callback = function () use ($filePath) {
|
||||
if (file_exists($filePath)) {
|
||||
unlink($filePath);
|
||||
}
|
||||
};
|
||||
|
||||
// We watch both app terminate and php shutdown to cover both normal app termination
|
||||
// as well as other potential scenarios (connection termination).
|
||||
app()->terminating($callback);
|
||||
register_shutdown_function($callback);
|
||||
}
|
||||
|
||||
return $this->streamedDirectly($stream, $fileName, $fileSize);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser,
|
||||
@@ -43,7 +70,7 @@ class DownloadResponseFactory
|
||||
public function streamedInline($stream, string $fileName, int $fileSize): StreamedResponse
|
||||
{
|
||||
$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());
|
||||
|
||||
return response()->stream(
|
||||
@@ -53,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.
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,13 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PreventResponseCaching
|
||||
{
|
||||
/**
|
||||
* Paths to ignore when preventing response caching.
|
||||
*/
|
||||
protected array $ignoredPathPrefixes = [
|
||||
'theme/',
|
||||
];
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
@@ -20,6 +27,13 @@ class PreventResponseCaching
|
||||
/** @var Response $response */
|
||||
$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('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ class RangeSupportedStream
|
||||
/**
|
||||
* Sniff a mime type from the stream.
|
||||
*/
|
||||
public function sniffMime(): string
|
||||
public function sniffMime(string $extension = ''): string
|
||||
{
|
||||
$offset = min(2000, $this->fileSize);
|
||||
$this->sniffContent = fread($this->stream, $offset);
|
||||
|
||||
return (new WebSafeMimeSniffer())->sniff($this->sniffContent);
|
||||
return (new WebSafeMimeSniffer())->sniff($this->sniffContent, $extension);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,13 @@ class SearchIndex
|
||||
/**
|
||||
* 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(
|
||||
protected EntityProvider $entityProvider
|
||||
@@ -196,15 +202,36 @@ class SearchIndex
|
||||
protected function textToTermCountMap(string $text): array
|
||||
{
|
||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||
$splitChars = static::$delimiters;
|
||||
$token = strtok($text, $splitChars);
|
||||
$softDelims = static::$softDelimiters;
|
||||
$tokenizer = new SearchTextTokenizer($text, static::$delimiters);
|
||||
$extendedToken = '';
|
||||
$extendedLen = 0;
|
||||
|
||||
$token = $tokenizer->next();
|
||||
|
||||
while ($token !== false) {
|
||||
if (!isset($tokenMap[$token])) {
|
||||
$tokenMap[$token] = 0;
|
||||
$delim = $tokenizer->previousDelimiter();
|
||||
|
||||
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;
|
||||
|
||||
@@ -181,7 +181,7 @@ class SearchOptions
|
||||
protected static function parseStandardTermString(string $termString): array
|
||||
{
|
||||
$terms = explode(' ', $termString);
|
||||
$indexDelimiters = SearchIndex::$delimiters;
|
||||
$indexDelimiters = implode('', array_diff(str_split(SearchIndex::$delimiters), str_split(SearchIndex::$softDelimiters)));
|
||||
$parsed = [
|
||||
'terms' => [],
|
||||
'exacts' => [],
|
||||
|
||||
70
app/Search/SearchTextTokenizer.php
Normal file
70
app/Search/SearchTextTokenizer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Controllers;
|
||||
namespace BookStack\Sorting;
|
||||
|
||||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Entities\Queries\BookQueries;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\BookSortMap;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Http\Controller;
|
||||
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);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$loggedActivityForBook = false;
|
||||
|
||||
// Return if no map sent
|
||||
if (!$request->filled('sort-tree')) {
|
||||
return redirect($book->getUrl());
|
||||
// Sort via map
|
||||
if ($request->filled('sort-tree')) {
|
||||
$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'));
|
||||
$bookContents = new BookContents($book);
|
||||
$booksInvolved = $bookContents->sortUsingMap($sortMap);
|
||||
|
||||
// Rebuild permissions and add activity for involved books.
|
||||
foreach ($booksInvolved as $bookInvolved) {
|
||||
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
|
||||
if ($request->filled('auto-sort')) {
|
||||
$sortSetId = intval($request->get('auto-sort')) ?: null;
|
||||
if ($sortSetId && SortRule::query()->find($sortSetId) === null) {
|
||||
$sortSetId = null;
|
||||
}
|
||||
$book->sort_rule_id = $sortSetId;
|
||||
$book->save();
|
||||
$sorter->runBookAutoSort($book);
|
||||
if (!$loggedActivityForBook) {
|
||||
Activity::add(ActivityType::BOOK_SORT, $book);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect($book->getUrl());
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
namespace BookStack\Sorting;
|
||||
|
||||
class BookSortMap
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
namespace BookStack\Sorting;
|
||||
|
||||
class BookSortMapItem
|
||||
{
|
||||
284
app/Sorting/BookSorter.php
Normal file
284
app/Sorting/BookSorter.php
Normal 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
63
app/Sorting/SortRule.php
Normal 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();
|
||||
}
|
||||
}
|
||||
114
app/Sorting/SortRuleController.php
Normal file
114
app/Sorting/SortRuleController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
69
app/Sorting/SortRuleOperation.php
Normal file
69
app/Sorting/SortRuleOperation.php
Normal 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);
|
||||
}
|
||||
}
|
||||
71
app/Sorting/SortSetOperationComparisons.php
Normal file
71
app/Sorting/SortSetOperationComparisons.php
Normal 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
49
app/Sorting/SortUrl.php
Normal 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));
|
||||
}
|
||||
}
|
||||
31
app/Theming/ThemeController.php
Normal file
31
app/Theming/ThemeController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,15 @@ class ThemeService
|
||||
*/
|
||||
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,
|
||||
* setting up the action to be ran when the event occurs.
|
||||
@@ -84,7 +93,7 @@ class ThemeService
|
||||
/**
|
||||
* @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->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
|
||||
|
||||
@@ -47,6 +47,7 @@ class LocaleManager
|
||||
'ja' => 'ja',
|
||||
'ka' => 'ka_GE',
|
||||
'ko' => 'ko_KR',
|
||||
'ku' => 'ku_TR',
|
||||
'lt' => 'lt_LT',
|
||||
'lv' => 'lv_LV',
|
||||
'nb' => 'nb_NO',
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Util\FilePathNormalizer;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Filesystem\FilesystemManager;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Flysystem\WhitespacePathNormalizer;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class FileStorage
|
||||
@@ -121,12 +120,13 @@ class FileStorage
|
||||
*/
|
||||
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') {
|
||||
return $path;
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
return 'uploads/files/' . $path;
|
||||
return 'uploads/files/' . $normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ class ImageRepo
|
||||
string $type,
|
||||
int $page = 0,
|
||||
int $pageSize = 24,
|
||||
int $uploadedTo = null,
|
||||
string $search = null,
|
||||
callable $whereClause = null
|
||||
?int $uploadedTo = null,
|
||||
?string $search = null,
|
||||
?callable $whereClause = null
|
||||
): array {
|
||||
$imageQuery = Image::query()->where('type', '=', strtolower($type));
|
||||
|
||||
@@ -91,7 +91,7 @@ class ImageRepo
|
||||
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
|
||||
if ($filterType === 'page') {
|
||||
$query->where('uploaded_to', '=', $contextPage->id);
|
||||
} elseif ($filterType === 'book') {
|
||||
} else if ($filterType === 'book') {
|
||||
$validPageIds = $contextPage->book->pages()
|
||||
->scopes('visible')
|
||||
->pluck('id')
|
||||
@@ -109,8 +109,14 @@ class ImageRepo
|
||||
*
|
||||
* @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);
|
||||
|
||||
if ($type !== 'system') {
|
||||
@@ -184,7 +190,7 @@ class ImageRepo
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyImage(Image $image = null): void
|
||||
public function destroyImage(?Image $image = null): void
|
||||
{
|
||||
if ($image) {
|
||||
$this->imageService->destroy($image);
|
||||
|
||||
@@ -158,7 +158,10 @@ class ImageResizer
|
||||
*/
|
||||
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
|
||||
// handling since we don't need the added animation support.
|
||||
|
||||
@@ -31,8 +31,8 @@ class ImageService
|
||||
UploadedFile $uploadedFile,
|
||||
string $type,
|
||||
int $uploadedTo = 0,
|
||||
int $resizeWidth = null,
|
||||
int $resizeHeight = null,
|
||||
?int $resizeWidth = null,
|
||||
?int $resizeHeight = null,
|
||||
bool $keepRatio = true,
|
||||
string $imageName = '',
|
||||
): Image {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Util\FilePathNormalizer;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use League\Flysystem\WhitespacePathNormalizer;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ImageStorageDisk
|
||||
@@ -30,13 +30,14 @@ class ImageStorageDisk
|
||||
*/
|
||||
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()) {
|
||||
return $path;
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
return 'uploads/images/' . $path;
|
||||
return 'uploads/images/' . $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@ class UserApiController extends ApiController
|
||||
});
|
||||
}
|
||||
|
||||
protected function rules(int $userId = null): array
|
||||
protected function rules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'create' => [
|
||||
@@ -54,7 +54,7 @@ class UserApiController extends ApiController
|
||||
'string',
|
||||
'email',
|
||||
'min:2',
|
||||
(new Unique('users', 'email'))->ignore($userId ?? null),
|
||||
(new Unique('users', 'email'))->ignore($userId),
|
||||
],
|
||||
'external_auth_id' => ['string'],
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
|
||||
17
app/Util/FilePathNormalizer.php
Normal file
17
app/Util/FilePathNormalizer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,16 @@ namespace BookStack\Util;
|
||||
|
||||
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
|
||||
{
|
||||
protected string $config;
|
||||
|
||||
public function __construct(string $config = null)
|
||||
public function __construct(?string $config = null)
|
||||
{
|
||||
$this->config = $config ?? config('app.ssr_hosts') ?? '';
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class WebSafeMimeSniffer
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $safeMimes = [
|
||||
protected array $safeMimes = [
|
||||
'application/json',
|
||||
'application/octet-stream',
|
||||
'application/pdf',
|
||||
@@ -48,16 +48,28 @@ class WebSafeMimeSniffer
|
||||
'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
|
||||
* through an allow-list to ensure a web-safe result.
|
||||
* 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);
|
||||
$mime = $fInfo->buffer($content) ?: 'application/octet-stream';
|
||||
|
||||
if ($mime === 'text/plain' && $extension) {
|
||||
$mime = $this->textTypesByExtension[$extension] ?? 'text/plain';
|
||||
}
|
||||
|
||||
if (in_array($mime, $this->safeMimes)) {
|
||||
return $mime;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^8.1.0",
|
||||
"php": "^8.2.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
@@ -18,12 +18,11 @@
|
||||
"ext-xml": "*",
|
||||
"ext-zip": "*",
|
||||
"bacon/bacon-qr-code": "^3.0",
|
||||
"doctrine/dbal": "^3.5",
|
||||
"dompdf/dompdf": "^3.0",
|
||||
"dompdf/dompdf": "^3.1",
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"intervention/image": "^3.5",
|
||||
"knplabs/knp-snappy": "^1.5",
|
||||
"laravel/framework": "^10.48.23",
|
||||
"laravel/framework": "^v11.37",
|
||||
"laravel/socialite": "^5.10",
|
||||
"laravel/tinker": "^2.8",
|
||||
"league/commonmark": "^2.3",
|
||||
@@ -40,17 +39,17 @@
|
||||
"socialiteproviders/okta": "^4.2",
|
||||
"socialiteproviders/twitch": "^5.3",
|
||||
"ssddanbrown/htmldiff": "^1.0.2",
|
||||
"ssddanbrown/symfony-mailer": "6.4.x-dev"
|
||||
"ssddanbrown/symfony-mailer": "7.2.x-dev"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.21",
|
||||
"itsgoingd/clockwork": "^5.1",
|
||||
"mockery/mockery": "^1.5",
|
||||
"nunomaduro/collision": "^7.0",
|
||||
"larastan/larastan": "^2.7",
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"nunomaduro/collision": "^8.1",
|
||||
"larastan/larastan": "^v3.0",
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"ssddanbrown/asserthtml": "^3.0"
|
||||
"ssddanbrown/asserthtml": "^3.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -104,7 +103,7 @@
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "8.1.0"
|
||||
"php": "8.2.0"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
|
||||
2583
composer.lock
generated
2583
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,9 @@ class BookFactory extends Factory
|
||||
'name' => $this->faker->sentence(),
|
||||
'slug' => Str::random(10),
|
||||
'description' => $description,
|
||||
'description_html' => '<p>' . e($description) . '</p>'
|
||||
'description_html' => '<p>' . e($description) . '</p>',
|
||||
'sort_rule_id' => null,
|
||||
'default_template_id' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
30
database/factories/Sorting/SortRuleFactory.php
Normal file
30
database/factories/Sorting/SortRuleFactory.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -26,25 +26,19 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$sm = Schema::getConnection()->getDoctrineSchemaManager();
|
||||
$prefix = DB::getTablePrefix();
|
||||
$pages = $sm->introspectTable($prefix . 'pages');
|
||||
$books = $sm->introspectTable($prefix . 'books');
|
||||
$chapters = $sm->introspectTable($prefix . 'chapters');
|
||||
|
||||
if ($pages->hasIndex('search')) {
|
||||
if (Schema::hasIndex('pages', 'search')) {
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
});
|
||||
}
|
||||
|
||||
if ($books->hasIndex('search')) {
|
||||
if (Schema::hasIndex('books', 'search')) {
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
});
|
||||
}
|
||||
|
||||
if ($chapters->hasIndex('search')) {
|
||||
if (Schema::hasIndex('chapters', 'search')) {
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
});
|
||||
|
||||
@@ -26,25 +26,19 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$sm = Schema::getConnection()->getDoctrineSchemaManager();
|
||||
$prefix = DB::getTablePrefix();
|
||||
$pages = $sm->introspectTable($prefix . 'pages');
|
||||
$books = $sm->introspectTable($prefix . 'books');
|
||||
$chapters = $sm->introspectTable($prefix . 'chapters');
|
||||
|
||||
if ($pages->hasIndex('name_search')) {
|
||||
if (Schema::hasIndex('pages', 'name_search')) {
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
}
|
||||
|
||||
if ($books->hasIndex('name_search')) {
|
||||
if (Schema::hasIndex('books', 'name_search')) {
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
}
|
||||
|
||||
if ($chapters->hasIndex('name_search')) {
|
||||
if (Schema::hasIndex('chapters', 'name_search')) {
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
|
||||
@@ -25,27 +25,21 @@ return new class extends Migration
|
||||
$table->index('score');
|
||||
});
|
||||
|
||||
$sm = Schema::getConnection()->getDoctrineSchemaManager();
|
||||
$prefix = DB::getTablePrefix();
|
||||
$pages = $sm->introspectTable($prefix . 'pages');
|
||||
$books = $sm->introspectTable($prefix . 'books');
|
||||
$chapters = $sm->introspectTable($prefix . 'chapters');
|
||||
|
||||
if ($pages->hasIndex('search')) {
|
||||
if (Schema::hasIndex('pages', 'search')) {
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
}
|
||||
|
||||
if ($books->hasIndex('search')) {
|
||||
if (Schema::hasIndex('books', 'search')) {
|
||||
Schema::table('books', function (Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
$table->dropIndex('name_search');
|
||||
});
|
||||
}
|
||||
|
||||
if ($chapters->hasIndex('search')) {
|
||||
if (Schema::hasIndex('chapters', 'search')) {
|
||||
Schema::table('chapters', function (Blueprint $table) {
|
||||
$table->dropIndex('search');
|
||||
$table->dropIndex('name_search');
|
||||
|
||||
@@ -8,7 +8,7 @@ return new class extends Migration
|
||||
/**
|
||||
* Mapping of old polymorphic types to new simpler values.
|
||||
*/
|
||||
protected $changeMap = [
|
||||
protected array $changeMap = [
|
||||
'BookStack\\Bookshelf' => 'bookshelf',
|
||||
'BookStack\\Book' => 'book',
|
||||
'BookStack\\Chapter' => 'chapter',
|
||||
@@ -18,7 +18,7 @@ return new class extends Migration
|
||||
/**
|
||||
* Mapping of tables and columns that contain polymorphic types.
|
||||
*/
|
||||
protected $columnsByTable = [
|
||||
protected array $columnsByTable = [
|
||||
'activities' => 'entity_type',
|
||||
'comments' => 'entity_type',
|
||||
'deletions' => 'deletable_type',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
1
dev/checksums/.gitignore
vendored
Normal file
1
dev/checksums/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
dev/checksums/vendor
Normal file
1
dev/checksums/vendor
Normal file
@@ -0,0 +1 @@
|
||||
8a38475da650a17612f885d8c650c835416ee53249ed83cfd79e6607f86e4ffa
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -47,6 +49,7 @@ This method allows you to register a custom social authentication driver within
|
||||
- string $driverName
|
||||
- array $config
|
||||
- string $socialiteHandler
|
||||
- callable|null $configureForRedirect = null
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -32,3 +34,24 @@ return [
|
||||
'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.
|
||||
|
||||
@@ -127,6 +127,13 @@ Copyright: Copyright (c) 2023 ECMAScript Shims
|
||||
Source: git+https://github.com/es-shims/ArrayBuffer.prototype.slice.git
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/async/LICENSE
|
||||
@@ -239,6 +246,13 @@ Copyright: Copyright (c) 2016, 2018 Linus Unnebäck
|
||||
Source: 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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/callsites/license
|
||||
@@ -303,6 +324,7 @@ Link: https://github.com/watson/ci-info
|
||||
cjs-module-lexer
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/convert-source-map/LICENSE
|
||||
@@ -427,22 +442,22 @@ data-view-buffer
|
||||
License: MIT
|
||||
License File: node_modules/data-view-buffer/LICENSE
|
||||
Copyright: Copyright (c) 2023 Jordan Harband
|
||||
Source: git+https://github.com/ljharb/data-view-buffer.git
|
||||
Link: https://github.com/ljharb/data-view-buffer#readme
|
||||
Source: git+https://github.com/inspect-js/data-view-buffer.git
|
||||
Link: https://github.com/inspect-js/data-view-buffer#readme
|
||||
-----------
|
||||
data-view-byte-length
|
||||
License: MIT
|
||||
License File: node_modules/data-view-byte-length/LICENSE
|
||||
Copyright: Copyright (c) 2024 Jordan Harband
|
||||
Source: git+https://github.com/ljharb/data-view-byte-length.git
|
||||
Link: https://github.com/ljharb/data-view-byte-length#readme
|
||||
Source: git+https://github.com/inspect-js/data-view-byte-length.git
|
||||
Link: https://github.com/inspect-js/data-view-byte-length#readme
|
||||
-----------
|
||||
data-view-byte-offset
|
||||
License: MIT
|
||||
License File: node_modules/data-view-byte-offset/LICENSE
|
||||
Copyright: Copyright (c) 2024 Jordan Harband
|
||||
Source: git+https://github.com/ljharb/data-view-byte-offset.git
|
||||
Link: https://github.com/ljharb/data-view-byte-offset#readme
|
||||
Source: git+https://github.com/inspect-js/data-view-byte-offset.git
|
||||
Link: https://github.com/inspect-js/data-view-byte-offset#readme
|
||||
-----------
|
||||
debug
|
||||
License: MIT
|
||||
@@ -546,6 +561,13 @@ License File: node_modules/domexception/LICENSE.txt
|
||||
Source: 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
|
||||
License: Apache-2.0
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/eslint-import-resolver-node/LICENSE
|
||||
@@ -696,14 +711,14 @@ eslint-scope
|
||||
License: BSD-2-Clause
|
||||
License File: node_modules/eslint-scope/LICENSE
|
||||
Copyright: Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors.
|
||||
Source: eslint/eslint-scope
|
||||
Link: http://github.com/eslint/eslint-scope
|
||||
Source: eslint/js
|
||||
Link: https://github.com/eslint/js/blob/main/packages/eslint-scope/README.md
|
||||
-----------
|
||||
eslint-visitor-keys
|
||||
License: Apache-2.0
|
||||
License File: node_modules/eslint-visitor-keys/LICENSE
|
||||
Source: eslint/eslint-visitor-keys
|
||||
Link: https://github.com/eslint/eslint-visitor-keys#readme
|
||||
Source: eslint/js
|
||||
Link: https://github.com/eslint/js/blob/main/packages/eslint-visitor-keys/README.md
|
||||
-----------
|
||||
eslint
|
||||
License: MIT
|
||||
@@ -716,8 +731,8 @@ License: BSD-2-Clause
|
||||
License File: node_modules/espree/LICENSE
|
||||
Copyright: Copyright (c) Open JS Foundation
|
||||
All rights reserved.
|
||||
Source: eslint/espree
|
||||
Link: https://github.com/eslint/espree
|
||||
Source: eslint/js
|
||||
Link: https://github.com/eslint/js/blob/main/packages/espree/README.md
|
||||
-----------
|
||||
esprima
|
||||
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
|
||||
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
|
||||
License: Apache-2.0
|
||||
Source: git@github.com:facebook/watchman.git
|
||||
@@ -805,9 +813,9 @@ Link: https://facebook.github.io/watchman/
|
||||
file-entry-cache
|
||||
License: MIT
|
||||
License File: node_modules/file-entry-cache/LICENSE
|
||||
Copyright: Copyright (c) 2015 Roy Riojas
|
||||
Source: royriojas/file-entry-cache
|
||||
Link: royriojas/file-entry-cache
|
||||
Copyright: Copyright (c) Roy Riojas & Jared Wray
|
||||
Source: jaredwray/file-entry-cache
|
||||
Link: jaredwray/file-entry-cache
|
||||
-----------
|
||||
filelist
|
||||
License: Apache-2.0
|
||||
@@ -846,7 +854,7 @@ for-each
|
||||
License: MIT
|
||||
License File: node_modules/for-each/LICENSE
|
||||
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
|
||||
-----------
|
||||
form-data
|
||||
@@ -912,6 +920,13 @@ Copyright: Copyright (c) 2020 CFWare, LLC
|
||||
Source: git+https://github.com/cfware/get-package-type.git
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/is-bigint/LICENSE
|
||||
@@ -1195,6 +1210,13 @@ Copyright: Copyright (c) 2014-2016, Jon Schlinkert
|
||||
Source: 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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/is-glob/LICENSE
|
||||
@@ -1216,12 +1245,12 @@ Copyright: Copyright (c) 2014-2017, Jon Schlinkert.
|
||||
Source: micromatch/is-glob
|
||||
Link: https://github.com/micromatch/is-glob
|
||||
-----------
|
||||
is-negative-zero
|
||||
is-map
|
||||
License: MIT
|
||||
License File: node_modules/is-negative-zero/LICENSE
|
||||
Copyright: Copyright (c) 2014 Jordan Harband
|
||||
Source: git://github.com/inspect-js/is-negative-zero.git
|
||||
Link: https://github.com/inspect-js/is-negative-zero
|
||||
License File: node_modules/is-map/LICENSE
|
||||
Copyright: Copyright (c) 2019 Inspect JS
|
||||
Source: git+https://github.com/inspect-js/is-map.git
|
||||
Link: https://github.com/inspect-js/is-map#readme
|
||||
-----------
|
||||
is-number-object
|
||||
License: MIT
|
||||
@@ -1237,13 +1266,6 @@ Copyright: Copyright (c) 2014-present, Jon Schlinkert.
|
||||
Source: 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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/is-shared-array-buffer/LICENSE
|
||||
@@ -1275,8 +1304,8 @@ is-string
|
||||
License: MIT
|
||||
License File: node_modules/is-string/LICENSE
|
||||
Copyright: Copyright (c) 2015 Jordan Harband
|
||||
Source: git://github.com/ljharb/is-string.git
|
||||
Link: git://github.com/ljharb/is-string.git
|
||||
Source: git://github.com/inspect-js/is-string.git
|
||||
Link: git://github.com/inspect-js/is-string.git
|
||||
-----------
|
||||
is-symbol
|
||||
License: MIT
|
||||
@@ -1292,6 +1321,13 @@ Copyright: Copyright (c) 2015 Jordan Harband
|
||||
Source: 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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/make-dir/license
|
||||
@@ -1743,6 +1793,13 @@ Copyright: Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.
|
||||
Source: 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
|
||||
License: MIT
|
||||
License File: node_modules/mdurl/LICENSE
|
||||
@@ -1903,13 +1960,6 @@ Copyright: Copyright (c) 2014 Jordan Harband
|
||||
Source: 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
|
||||
License: MIT
|
||||
License File: node_modules/object.fromentries/LICENSE
|
||||
@@ -1960,6 +2010,13 @@ All rights reserved.
|
||||
Source: github:khtdr/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
|
||||
License: MIT
|
||||
License File: node_modules/p-limit/license
|
||||
@@ -2000,7 +2057,7 @@ License: MIT
|
||||
License File: node_modules/parse5/LICENSE
|
||||
Copyright: Copyright (c) 2013-2019 Ivan Nikulin (******@*****.***, https://github.com/inikulin)
|
||||
Source: git://github.com/inikulin/parse5.git
|
||||
Link: https://github.com/inikulin/parse5
|
||||
Link: https://parse5.js.org
|
||||
-----------
|
||||
path-exists
|
||||
License: MIT
|
||||
@@ -2140,13 +2197,6 @@ Copyright: Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.
|
||||
Source: 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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
Link: git://github.com/browserify/resolve.git
|
||||
-----------
|
||||
reusify
|
||||
License: MIT
|
||||
License File: node_modules/reusify/LICENSE
|
||||
Copyright: Copyright (c) 2015 Matteo Collina
|
||||
Source: git+https://github.com/mcollina/reusify.git
|
||||
Link: https://github.com/mcollina/reusify#readme
|
||||
rollup-plugin-dts
|
||||
License: LGPL-3.0
|
||||
Source: git+https://github.com/Swatinem/rollup-plugin-dts.git
|
||||
Link: https://github.com/Swatinem/rollup-plugin-dts#readme
|
||||
-----------
|
||||
rimraf
|
||||
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
|
||||
rollup
|
||||
License: MIT
|
||||
License File: node_modules/run-parallel/LICENSE
|
||||
Copyright: Copyright (c) Feross Aboukhadijeh
|
||||
Source: git://github.com/feross/run-parallel.git
|
||||
Link: https://github.com/feross/run-parallel
|
||||
License File: node_modules/rollup/LICENSE.md
|
||||
Copyright: Copyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)
|
||||
Source: rollup/rollup
|
||||
Link: https://rollupjs.org/
|
||||
-----------
|
||||
safe-array-concat
|
||||
License: MIT
|
||||
@@ -2252,6 +2300,13 @@ Copyright: Copyright (c) 2023 Jordan Harband
|
||||
Source: git+https://github.com/ljharb/safe-array-concat.git
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: BSD-3-Clause
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/to-regex-range/LICENSE
|
||||
@@ -2579,7 +2648,7 @@ Link: https://github.com/jsdom/tr46
|
||||
ts-jest
|
||||
License: MIT
|
||||
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
|
||||
Link: https://kulshekhar.github.io/ts-jest
|
||||
-----------
|
||||
@@ -2622,8 +2691,8 @@ typed-array-buffer
|
||||
License: MIT
|
||||
License File: node_modules/typed-array-buffer/LICENSE
|
||||
Copyright: Copyright (c) 2023 Jordan Harband
|
||||
Source: git+https://github.com/ljharb/typed-array-buffer.git
|
||||
Link: https://github.com/ljharb/typed-array-buffer#readme
|
||||
Source: git+https://github.com/inspect-js/typed-array-buffer.git
|
||||
Link: https://github.com/inspect-js/typed-array-buffer#readme
|
||||
-----------
|
||||
typed-array-byte-length
|
||||
License: MIT
|
||||
@@ -2774,6 +2843,20 @@ Copyright: Copyright (c) 2019 Jordan Harband
|
||||
Source: git+https://github.com/inspect-js/which-boxed-primitive.git
|
||||
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
|
||||
License: ISC
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/@babel/parser/LICENSE
|
||||
@@ -3282,6 +3351,18 @@ Copyright: Copyright (c) 2018 Toru Nagashima
|
||||
Source: https://github.com/eslint-community/regexpp
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
Link: https://eslint.org
|
||||
-----------
|
||||
@humanwhocodes/config-array
|
||||
@eslint/object-schema
|
||||
License: Apache-2.0
|
||||
License File: node_modules/@humanwhocodes/config-array/LICENSE
|
||||
Source: git+https://github.com/humanwhocodes/config-array.git
|
||||
Link: https://github.com/humanwhocodes/config-array#readme
|
||||
License File: node_modules/@eslint/object-schema/LICENSE
|
||||
Source: git+https://github.com/eslint/rewrite.git
|
||||
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
|
||||
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
|
||||
Link: git+https://github.com/humanwhocodes/module-importer.git
|
||||
-----------
|
||||
@humanwhocodes/object-schema
|
||||
License: BSD-3-Clause
|
||||
License File: node_modules/@humanwhocodes/object-schema/LICENSE
|
||||
Copyright: Copyright (c) 2019, Human Who Codes
|
||||
All rights reserved.
|
||||
Source: git+https://github.com/humanwhocodes/object-schema.git
|
||||
Link: https://github.com/humanwhocodes/object-schema#readme
|
||||
@humanwhocodes/retry
|
||||
License: Apache-2.0
|
||||
License File: node_modules/@humanwhocodes/retry/LICENSE
|
||||
Source: git+https://github.com/humanwhocodes/retry.git
|
||||
Link: git+https://github.com/humanwhocodes/retry.git
|
||||
-----------
|
||||
@istanbuljs/load-nyc-config
|
||||
License: ISC
|
||||
@@ -3538,32 +3635,20 @@ Copyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and
|
||||
Source: 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
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
Link: https://github.com/parcel-bundler/watcher.git
|
||||
-----------
|
||||
@parcel/watcher-linux-x64-musl
|
||||
License: MIT
|
||||
License File: node_modules/@parcel/watcher-linux-x64-musl/LICENSE
|
||||
Copyright: Copyright (c) 2017-present Devon Govett
|
||||
Source: https://github.com/parcel-bundler/watcher.git
|
||||
Link: https://github.com/parcel-bundler/watcher.git
|
||||
-----------
|
||||
@parcel/watcher
|
||||
License: MIT
|
||||
License File: node_modules/@parcel/watcher/LICENSE
|
||||
@@ -3686,6 +3764,13 @@ Copyright: Copyright (c) Microsoft Corporation.
|
||||
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/@types/graceful-fs/LICENSE
|
||||
@@ -3728,11 +3813,25 @@ Copyright: Copyright (c) Microsoft Corporation.
|
||||
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
|
||||
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
|
||||
License: MIT
|
||||
Source: 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
|
||||
License: MIT
|
||||
License File: node_modules/@types/node/LICENSE
|
||||
@@ -3740,6 +3839,13 @@ Copyright: Copyright (c) Microsoft Corporation.
|
||||
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
|
||||
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
|
||||
License: MIT
|
||||
License File: node_modules/@types/stack-utils/LICENSE
|
||||
@@ -3767,10 +3873,3 @@ License File: node_modules/@types/yargs/LICENSE
|
||||
Copyright: Copyright (c) Microsoft Corporation.
|
||||
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
|
||||
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
|
||||
|
||||
@@ -47,34 +47,6 @@ Copyright: Copyright (c) 2012 Dragonfly Development Inc.
|
||||
Source: https://github.com/dflydev/dflydev-dot-access-data.git
|
||||
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
|
||||
License: MIT
|
||||
License File: vendor/doctrine/inflector/LICENSE
|
||||
@@ -195,7 +167,7 @@ Link: https://github.com/guzzle/uri-template.git
|
||||
intervention/gif
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: vendor/masterminds/html5/LICENSE.txt
|
||||
@@ -336,7 +322,7 @@ nesbot/carbon
|
||||
License: MIT
|
||||
License File: vendor/nesbot/carbon/LICENSE
|
||||
Copyright: Copyright (C) Brian Nesbitt
|
||||
Source: https://github.com/briannesbitt/Carbon.git
|
||||
Source: https://github.com/CarbonPHP/carbon.git
|
||||
Link: https://carbon.nesbot.com
|
||||
-----------
|
||||
nette/schema
|
||||
@@ -419,13 +405,6 @@ Copyright (c) 2021-2024 Till Krüss (modified work)
|
||||
Source: https://github.com/predis/predis.git
|
||||
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
|
||||
License: MIT
|
||||
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
|
||||
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
|
||||
License: MIT
|
||||
License File: vendor/symfony/console/LICENSE
|
||||
|
||||
64
eslint.config.mjs
Normal file
64
eslint.config.mjs
Normal 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,
|
||||
}],
|
||||
},
|
||||
}];
|
||||
@@ -85,12 +85,12 @@ return [
|
||||
'webhook_delete_notification' => 'تم حذف Webhook بنجاح',
|
||||
|
||||
// Imports
|
||||
'import_create' => 'created import',
|
||||
'import_create_notification' => 'Import successfully uploaded',
|
||||
'import_run' => 'updated import',
|
||||
'import_run_notification' => 'Content successfully imported',
|
||||
'import_delete' => 'deleted import',
|
||||
'import_delete_notification' => 'Import successfully deleted',
|
||||
'import_create' => 'تم إنشاء الاستيراد',
|
||||
'import_create_notification' => 'تم رفع الاستيراد بنجاح',
|
||||
'import_run' => 'تم تحديث الاستيراد',
|
||||
'import_run_notification' => 'تم استيراد المحتوى بنجاح',
|
||||
'import_delete' => 'تم حذف الاستيراد',
|
||||
'import_delete_notification' => 'تم الاستيراد بنجاح',
|
||||
|
||||
// Users
|
||||
'user_create' => 'إنشاء مستخدم',
|
||||
@@ -101,11 +101,11 @@ return [
|
||||
'user_delete_notification' => 'تم إزالة المستخدم بنجاح',
|
||||
|
||||
// API Tokens
|
||||
'api_token_create' => 'created API token',
|
||||
'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-',
|
||||
'api_token_create_notification' => 'تم إنشاء رمز الـ API بنجاح',
|
||||
'api_token_update' => 'updated API token',
|
||||
'api_token_update' => 'رمز واجهة برمجة التطبيقات المحدث',
|
||||
'api_token_update_notification' => 'تم تحديث رمز الـ API بنجاح',
|
||||
'api_token_delete' => 'deleted API token',
|
||||
'api_token_delete' => 'رمز واجهة برمجة التطبيقات المحذوف',
|
||||
'api_token_delete_notification' => 'تم حذف رمز الـ API بنجاح',
|
||||
|
||||
// Roles
|
||||
@@ -127,6 +127,14 @@ return [
|
||||
'comment_update' => 'تعليق محدث',
|
||||
'comment_delete' => 'تعليق محذوف',
|
||||
|
||||
// Sort Rules
|
||||
'sort_rule_create' => 'تم إنشاء قاعدة الفرز',
|
||||
'sort_rule_create_notification' => 'تم إنشاء قاعدة الفرز بنجاح',
|
||||
'sort_rule_update' => 'تم تحديث قاعدة الفرز',
|
||||
'sort_rule_update_notification' => 'تم تحديث قاعدة الفرز بنجاح',
|
||||
'sort_rule_delete' => 'تم حذف قاعدة الفرز',
|
||||
'sort_rule_delete_notification' => 'تم حذف قاعدة الفرز بنجاح',
|
||||
|
||||
// Other
|
||||
'permissions_update' => 'تحديث الأذونات',
|
||||
];
|
||||
|
||||
@@ -87,31 +87,31 @@ return [
|
||||
'mfa_setup_reconfigure' => 'إعادة التكوين',
|
||||
'mfa_setup_remove_confirmation' => 'هل أنت متأكد من أنك تريد إزالة طريقة المصادقة متعددة العناصر هذه؟',
|
||||
'mfa_setup_action' => 'إعداد (تنصيب)',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'لديك أقل من 5 رموز احتياطية متبقية، الرجاء إنشاء وتخزين مجموعة جديدة قبل نفاد الرموز لتجنب إغلاق حسابك.',
|
||||
'mfa_option_totp_title' => 'تطبيق الجوال',
|
||||
'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق محمول يدعم TOTP مثل Google Authenticator أو Authy أو Microsoft Authenticer.',
|
||||
'mfa_option_backup_codes_title' => 'رموز النسخ الاحتياطي',
|
||||
'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',
|
||||
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
|
||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
||||
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
|
||||
'mfa_gen_backup_codes_download' => 'Download Codes',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
||||
'mfa_gen_totp_title' => 'Mobile App Setup',
|
||||
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
|
||||
'mfa_gen_totp_verify_setup' => 'Verify Setup',
|
||||
'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
|
||||
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
||||
'mfa_verify_access' => 'Verify Access',
|
||||
'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
|
||||
'mfa_verify_no_methods' => 'No Methods Configured',
|
||||
'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
|
||||
'mfa_verify_use_totp' => 'Verify using a mobile app',
|
||||
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
|
||||
'mfa_verify_backup_code' => 'Backup Code',
|
||||
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
|
||||
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
|
||||
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
|
||||
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
|
||||
'mfa_option_backup_codes_desc' => 'إنشاء مجموعة من رموز النسخ الاحتياطية للاستخدام مرة واحدة و التي سَتُدِخلها عند تسجيل الدخول للتحقق من هويتك. احرص أن تخزينها في مكان آمن.',
|
||||
'mfa_gen_confirm_and_enable' => 'تأكيد وتمكين',
|
||||
'mfa_gen_backup_codes_title' => 'إعداد رموز النسخ الاحتياطي',
|
||||
'mfa_gen_backup_codes_desc' => 'خَزِن قائمة الرموز أدناه في مكان آمن. عند الوصول إلى النظام، ستتمكن من استخدام أحد الرموز كآلية مصادقة ثانية.',
|
||||
'mfa_gen_backup_codes_download' => 'تنزيل الرموز',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'يمكن استخدام كل رمز مرة واحدة فقط',
|
||||
'mfa_gen_totp_title' => 'إعداد تطبيق الجوال',
|
||||
'mfa_gen_totp_desc' => 'لاستخدام المصادقة المتعددة ، ستحتاج إلى تطبيق جوال كلمة المرور المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator.-',
|
||||
'mfa_gen_totp_scan' => 'امسح رمز الاستجابة السريعة -QR- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.',
|
||||
'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد',
|
||||
'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:',
|
||||
'mfa_gen_totp_provide_code_here' => 'أدخل الرمز الذي تم إنشاؤه للتطبيق الخاص بك هنا',
|
||||
'mfa_verify_access' => 'التحقق من الوصول',
|
||||
'mfa_verify_access_desc' => 'يتطلب حساب المستخدم الخاص بك تأكيد هويتك عن طريق مستوى إضافي من التحقق قبل منحك حق الوصول. تحقق استخدام إحدى الطرق التي إعدادها للمتابعة.',
|
||||
'mfa_verify_no_methods' => 'لا توجد طرق معدة',
|
||||
'mfa_verify_no_methods_desc' => 'لم يتم العثور على طرق مصادقة متعددة العوامل لحسابك. ستحتاج إلى إعداد طريقة واحدة على الأقل قبل أن تتمكن من الوصول.',
|
||||
'mfa_verify_use_totp' => 'التحقق باستخدام تطبيق الجوال',
|
||||
'mfa_verify_use_backup_codes' => 'التحقق باستخدام رمز النسخ الاحتياطي',
|
||||
'mfa_verify_backup_code' => 'الرموز الاحتياطية',
|
||||
'mfa_verify_backup_code_desc' => 'أدخل أحد الرموز الاحتياطية المتبقية أدناه:',
|
||||
'mfa_verify_backup_code_enter_here' => 'أدخل الرمز الاحتياطي هنا',
|
||||
'mfa_verify_totp_desc' => 'أدخل الرمز الذي تم إنشاؤه باستخدام تطبيق الجوال الخاص بك، أدناه:',
|
||||
'mfa_setup_login_notification' => 'تم إعداد طريقة الدخول متعددة العوامل، يرجى الآن تسجيل الدخول مرة أخرى باستخدام الطريقة التي تم إعدادها.',
|
||||
];
|
||||
|
||||
@@ -20,7 +20,7 @@ return [
|
||||
'description' => 'الوصف',
|
||||
'role' => 'الدور',
|
||||
'cover_image' => 'صورة الغلاف',
|
||||
'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440x250 بكسل، على الرغم من أنه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.',
|
||||
'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440 في 250 بكسل، مع أنّه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.',
|
||||
|
||||
// Actions
|
||||
'actions' => 'إجراءات',
|
||||
@@ -48,8 +48,8 @@ return [
|
||||
'unfavourite' => 'إزالة من المفضلة',
|
||||
'next' => 'التالي',
|
||||
'previous' => 'السابق',
|
||||
'filter_active' => 'الفلاتر المفعلة:',
|
||||
'filter_clear' => 'مسح الفلاتر',
|
||||
'filter_active' => 'التصفية المفعلة:',
|
||||
'filter_clear' => 'مسح التصفية',
|
||||
'download' => 'تنزيل',
|
||||
'open_in_tab' => 'فتح في علامة تبويب',
|
||||
'open' => 'فتح',
|
||||
@@ -109,5 +109,5 @@ return [
|
||||
'terms_of_service' => 'اتفاقية شروط الخدمة',
|
||||
|
||||
// OpenSearch
|
||||
'opensearch_description' => 'Search :appName',
|
||||
'opensearch_description' => 'البحث عن :appName',
|
||||
];
|
||||
|
||||
@@ -6,36 +6,36 @@ return [
|
||||
|
||||
// Image Manager
|
||||
'image_select' => 'تحديد صورة',
|
||||
'image_list' => 'Image List',
|
||||
'image_details' => 'Image Details',
|
||||
'image_upload' => 'Upload Image',
|
||||
'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
|
||||
'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
|
||||
'image_list' => 'قائمة الصور',
|
||||
'image_details' => 'تفاصيل الصورة',
|
||||
'image_upload' => 'تحميل صورة',
|
||||
'image_intro' => 'هنا يمكنك تحديد وإدارة الصور التي تم تحميلها مسبقًا إلى النظام.',
|
||||
'image_intro_upload' => 'تحميل صورة جديدة عن طريق سحب الصورة إلى هذه النافذة، أو باستخدام زر "تحميل صورة" أعلاه.',
|
||||
'image_all' => 'الكل',
|
||||
'image_all_title' => 'عرض جميع الصور',
|
||||
'image_book_title' => 'عرض الصور المرفوعة لهذا الكتاب',
|
||||
'image_page_title' => 'عرض الصور المرفوعة لهذه الصفحة',
|
||||
'image_search_hint' => 'البحث باستخدام اسم الصورة',
|
||||
'image_uploaded' => 'وقت الرفع :uploadedDate',
|
||||
'image_uploaded_by' => 'Uploaded by :userName',
|
||||
'image_uploaded_to' => 'Uploaded to :pageLink',
|
||||
'image_updated' => 'Updated :updateDate',
|
||||
'image_uploaded_by' => 'تم تحميلها من قبل :userName',
|
||||
'image_uploaded_to' => 'تم رفعها إلى :pageLink',
|
||||
'image_updated' => 'تم تحديثها :updatedate',
|
||||
'image_load_more' => 'المزيد',
|
||||
'image_image_name' => 'اسم الصورة',
|
||||
'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',
|
||||
'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة؟',
|
||||
'image_select_image' => 'تحديد الصورة',
|
||||
'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
|
||||
'image_dropzone_drop' => 'Drop images here to upload',
|
||||
'image_dropzone_drop' => 'إسقاط صورة أو اضغط هنا للرفع',
|
||||
'images_deleted' => 'تم حذف الصور',
|
||||
'image_preview' => 'معاينة الصور',
|
||||
'image_upload_success' => 'تم رفع الصورة بنجاح',
|
||||
'image_update_success' => 'تم تحديث تفاصيل الصورة بنجاح',
|
||||
'image_delete_success' => 'تم حذف الصورة بنجاح',
|
||||
'image_replace' => 'Replace Image',
|
||||
'image_replace_success' => 'Image file successfully updated',
|
||||
'image_rebuild_thumbs' => 'Regenerate Size Variations',
|
||||
'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',
|
||||
'image_replace' => 'استبدال صورة',
|
||||
'image_replace_success' => 'تم تحديث الصورة بنجاح',
|
||||
'image_rebuild_thumbs' => 'تجديد تغيرات الحجم',
|
||||
'image_rebuild_thumbs_success' => 'تم إعادة بناء تغيرات حجم الصورة بنجاح!',
|
||||
|
||||
// Code Editor
|
||||
'code_editor' => 'تعديل الشفرة',
|
||||
|
||||
@@ -13,6 +13,7 @@ return [
|
||||
'cancel' => 'إلغاء',
|
||||
'save' => 'حفظ',
|
||||
'close' => 'إغلاق',
|
||||
'apply' => 'Apply',
|
||||
'undo' => 'تراجع',
|
||||
'redo' => 'إعادة التنفيذ',
|
||||
'left' => 'يسار',
|
||||
@@ -147,6 +148,7 @@ return [
|
||||
'url' => 'URL',
|
||||
'text_to_display' => 'Text to display',
|
||||
'title' => 'Title',
|
||||
'browse_links' => 'Browse links',
|
||||
'open_link' => 'Open link',
|
||||
'open_link_in' => 'Open link in...',
|
||||
'open_link_current' => 'Current window',
|
||||
|
||||
@@ -22,15 +22,15 @@ return [
|
||||
'meta_created_name' => 'أنشئ :timeLength بواسطة :user',
|
||||
'meta_updated' => 'مُحدث :timeLength',
|
||||
'meta_updated_name' => 'مُحدث :timeLength بواسطة :user',
|
||||
'meta_owned_name' => 'Owned by :user',
|
||||
'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',
|
||||
'meta_owned_name' => 'مملوكة لـ:user',
|
||||
'meta_reference_count' => 'مشار إليه :count مرة|مشار إليه :count مرة',
|
||||
'entity_select' => 'اختيار الكيان',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'entity_select_lack_permission' => 'ليس لديك الصلاحيات المطلوبة لتحديد هذا العنصر',
|
||||
'images' => 'صور',
|
||||
'my_recent_drafts' => 'مسوداتي الحديثة',
|
||||
'my_recently_viewed' => 'ما عرضته مؤخراً',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'my_most_viewed_favourites' => 'مفضلاتي الأكثر مشاهدة',
|
||||
'my_favourites' => 'مفضلاتي',
|
||||
'no_pages_viewed' => 'لم تستعرض أي صفحات',
|
||||
'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً',
|
||||
'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً',
|
||||
@@ -38,43 +38,43 @@ return [
|
||||
'export_html' => 'صفحة ويب',
|
||||
'export_pdf' => 'ملف PDF',
|
||||
'export_text' => 'ملف نص عادي',
|
||||
'export_md' => 'Markdown File',
|
||||
'export_zip' => 'Portable ZIP',
|
||||
'default_template' => 'Default Page Template',
|
||||
'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',
|
||||
'default_template_select' => 'Select a template page',
|
||||
'import' => 'Import',
|
||||
'import_validate' => 'Validate Import',
|
||||
'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\'ll be able to configure & confirm the import in the next view.',
|
||||
'import_zip_select' => 'Select ZIP file to upload',
|
||||
'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',
|
||||
'import_pending' => 'Pending Imports',
|
||||
'import_pending_none' => 'No imports have been started.',
|
||||
'import_continue' => 'Continue Import',
|
||||
'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',
|
||||
'import_details' => 'Import Details',
|
||||
'import_run' => 'Run Import',
|
||||
'import_size' => ':size Import ZIP Size',
|
||||
'import_uploaded_at' => 'Uploaded :relativeTime',
|
||||
'import_uploaded_by' => 'Uploaded by',
|
||||
'import_location' => 'Import Location',
|
||||
'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.',
|
||||
'import_delete_confirm' => 'Are you sure you want to delete this import?',
|
||||
'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',
|
||||
'import_errors' => 'Import Errors',
|
||||
'import_errors_desc' => 'The follow errors occurred during the import attempt:',
|
||||
'export_md' => 'ملف ماركداون -Markdown-',
|
||||
'export_zip' => 'ملف مضغوط -ZIP-',
|
||||
'default_template' => 'قالب الصفحة الافتراضية',
|
||||
'default_template_explain' => 'قم بتعيين قالب صفحة سيتم استخدامه كمحتوى افتراضي لجميع الصفحات التي تم إنشاؤها ضمن هذا العنصر. ضع في اعتبارك أن هذا لن يتم استخدامه إلا إذا كان لدى منشئ الصفحة حق الوصول إلى صفحة القالب المختارة.',
|
||||
'default_template_select' => 'حدد صفحة القالب',
|
||||
'import' => 'استيراد',
|
||||
'import_validate' => 'التحقق من صحة الاستيراد',
|
||||
'import_desc' => 'استيراد الكتب والفصول والصفحات باستخدام تصدير مِلَفّ مضغوط ZIP محمول من نفس النظام أو نظام مختلف. حدد مِلَفّ ZIP للمتابعة. بعد تحميل المِلَفّ والتحقق من صحته، ستتمكن من إعداد وتأكيد الاستيراد في العرض التالي.',
|
||||
'import_zip_select' => 'حدد مِلَفّ مضغوط بصيغة ZIP للتحميل',
|
||||
'import_zip_validation_errors' => 'تم اكتشاف أخطاء في أثناء التحقق من صحة المِلَفّ المضغوط ZIP المقدم:',
|
||||
'import_pending' => 'الاستيرادات المعلقة',
|
||||
'import_pending_none' => 'لم يتم البَدْء في أي عملية استيراد.',
|
||||
'import_continue' => 'متابعة الاستيراد',
|
||||
'import_continue_desc' => 'راجع المحتوى الذي يجب استيراده من المِلَفّ المضغوط ZIP الذي تم تحميله. عندما يكون جاهزًا، تشتغل عملية الاستيراد لإضافة محتوياته إلى هذا النظام. سيتم إزالة مِلَفّ الاستيراد الذي تم تحميله تلقائيًا عند الاستيراد الناجح.',
|
||||
'import_details' => 'تفاصيل الاستيراد',
|
||||
'import_run' => 'تشغيل الاستيراد',
|
||||
'import_size' => 'حجم الاستيراد :size ',
|
||||
'import_uploaded_at' => 'تم تحميلة في :relativeTime',
|
||||
'import_uploaded_by' => 'رُفِع بواسطة',
|
||||
'import_location' => 'موقع الاستيراد',
|
||||
'import_location_desc' => 'حدد موقعًا مستهدفًا للمحتوى المستورد. ستحتاج إلى الصلاحيات ذات الصلة لإنشاء المحتوى داخل الموقع الذي تختاره.',
|
||||
'import_delete_confirm' => 'متيقِّن من أنك تريد حذف الاستيراد؟',
|
||||
'import_delete_desc' => 'سيؤدي هذا إلى حذف مِلَفّ الاستيراد المضغوط ZIP، ولا يمكن التراجع عنه.',
|
||||
'import_errors' => 'أخطاء الاستيراد',
|
||||
'import_errors_desc' => 'حدثت الأخطاء التالية خلال محاولة الاستيراد:',
|
||||
|
||||
// Permissions and restrictions
|
||||
'permissions' => 'الأذونات',
|
||||
'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
|
||||
'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
|
||||
'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
|
||||
'permissions_desc' => 'تعيين الصلاحيات هنا لتجاوز الصلاحيات الافتراضية التي توفرها أدوار المستخدم.',
|
||||
'permissions_book_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها للكتب تلقائيًا إلى الفصول والصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.',
|
||||
'permissions_chapter_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها على الفصول تلقائيًا إلى الصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.',
|
||||
'permissions_save' => 'حفظ الأذونات',
|
||||
'permissions_owner' => 'Owner',
|
||||
'permissions_role_everyone_else' => 'Everyone Else',
|
||||
'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
|
||||
'permissions_role_override' => 'Override permissions for role',
|
||||
'permissions_inherit_defaults' => 'Inherit defaults',
|
||||
'permissions_owner' => 'المالك',
|
||||
'permissions_role_everyone_else' => 'الآخرين',
|
||||
'permissions_role_everyone_else_desc' => 'تعيين الصلاحيات لجميع الأدوار التي لم يتم تجاوزها على وجه التحديد.',
|
||||
'permissions_role_override' => 'تجاوز الصلاحيات للدور',
|
||||
'permissions_inherit_defaults' => 'وراثة الإعدادات الافتراضية',
|
||||
|
||||
// Search
|
||||
'search_results' => 'نتائج البحث',
|
||||
@@ -94,7 +94,7 @@ return [
|
||||
'search_permissions_set' => 'حزمة الأذونات',
|
||||
'search_created_by_me' => 'أنشئت بواسطتي',
|
||||
'search_updated_by_me' => 'حُدثت بواسطتي',
|
||||
'search_owned_by_me' => 'Owned by me',
|
||||
'search_owned_by_me' => 'مملوكة لي',
|
||||
'search_date_options' => 'خيارات التاريخ',
|
||||
'search_updated_before' => 'حدثت قبل',
|
||||
'search_updated_after' => 'حدثت بعد',
|
||||
@@ -117,24 +117,24 @@ return [
|
||||
'shelves_save' => 'حفظ الرف',
|
||||
'shelves_books' => 'كتب على هذا الرف',
|
||||
'shelves_add_books' => 'إضافة كتب لهذا الرف',
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_drag_books' => 'اسحب الكتب الموجودة بالأسفل لإضافتها إلى هذا الرف',
|
||||
'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',
|
||||
'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',
|
||||
'shelves_edit_named' => 'تعديل الرف :name',
|
||||
'shelves_edit' => 'تعديل الرف',
|
||||
'shelves_delete' => 'حذف الرف',
|
||||
'shelves_delete_named' => 'حذف الرف :name',
|
||||
'shelves_delete_explain' => "سيؤدي هذا إلى حذف الرف الذي يحمل الاسم ':name'. لن يتم حذف الكتب المضمنة بداخله.",
|
||||
'shelves_delete_confirmation' => 'هل أنت متأكد أنك تريد حذف هذا الرف؟',
|
||||
'shelves_permissions' => 'صلاحيات الرف',
|
||||
'shelves_permissions_updated' => 'تم تحديث صلاحيات الرف',
|
||||
'shelves_permissions_active' => 'صلاحيات الرف نشطة',
|
||||
'shelves_permissions_cascade_warning' => 'لا يتم نقل الصلاحيات الموجودة على الأرفف تلقائيًا إلى الكتب الموجودة في كل رف. وذلك لأن الكتاب يمكن أن يوجد على أرفف متعددة. ومع ذلك، يمكن نسخ الصلاحيات إلى الكتب الفرعية باستخدام الخِيار الموجود أدناه.',
|
||||
'shelves_permissions_create' => 'تُستخدم صلاحيات إنشاء الرفوف فقط لنسخ الصلاحيات إلى الكتب الفرعية باستخدام الإجراء أدناه. ولا تتحكم في القدرة على إنشاء الكتب.',
|
||||
'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
|
||||
'shelves_copy_permissions' => 'نسخ الأذونات',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الصلاحيات الحالية لهذا الرف على جميع الكتب الموجودة بداخله. قبل التنشيط، تأكد من حفظ أي تغييرات على صلاحيات هذا الرف.',
|
||||
'shelves_copy_permission_success' => 'تم نسخ صلاحيات الرف إلى :count كتاب/كتب',
|
||||
|
||||
// Books
|
||||
'book' => 'كتاب',
|
||||
@@ -166,7 +166,9 @@ return [
|
||||
'books_search_this' => 'البحث في هذا الكتاب',
|
||||
'books_navigation' => 'تصفح الكتاب',
|
||||
'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' => 'نقل الفصول والصفحات داخل الكتاب لإعادة تنظيم محتوياته. يمكن إضافة كتب أخرى مما يسمح بنقل الفصول والصفحات بسهولة بين الكتب. اختياريًا، يمكن تعيين قاعدة فرز تلقائي لفرز محتويات هذا الكتاب تلقائيًا عند حدوث تغييرات.',
|
||||
'books_sort_auto_sort' => 'خِيار الفرز التلقائي',
|
||||
'books_sort_auto_sort_active' => 'الفرز التلقائي الشَغَّال: :sortName',
|
||||
'books_sort_named' => 'فرز كتاب :bookName',
|
||||
'books_sort_name' => 'ترتيب حسب الإسم',
|
||||
'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء',
|
||||
@@ -175,19 +177,19 @@ return [
|
||||
'books_sort_chapters_last' => 'الفصول الأخيرة',
|
||||
'books_sort_show_other' => 'عرض كتب أخرى',
|
||||
'books_sort_save' => 'حفظ الترتيب الجديد',
|
||||
'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',
|
||||
'books_sort_move_up' => 'Move Up',
|
||||
'books_sort_move_down' => 'Move Down',
|
||||
'books_sort_move_prev_book' => 'Move to Previous Book',
|
||||
'books_sort_move_next_book' => 'Move to Next Book',
|
||||
'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',
|
||||
'books_sort_move_next_chapter' => 'Move Into Next Chapter',
|
||||
'books_sort_move_book_start' => 'Move to Start of Book',
|
||||
'books_sort_move_book_end' => 'Move to End of Book',
|
||||
'books_sort_move_before_chapter' => 'Move to Before Chapter',
|
||||
'books_sort_move_after_chapter' => 'Move to After Chapter',
|
||||
'books_copy' => 'Copy Book',
|
||||
'books_copy_success' => 'Book successfully copied',
|
||||
'books_sort_show_other_desc' => 'أضف كتبًا أخرى هنا لتضمينها في عملية الفرز، والسماح بإعادة تنظيم الكتب بسهولة.',
|
||||
'books_sort_move_up' => 'حرك للأعلى',
|
||||
'books_sort_move_down' => 'حرك للأسفل',
|
||||
'books_sort_move_prev_book' => 'نقل للكتاب السابق',
|
||||
'books_sort_move_next_book' => 'نقل للكتاب التالي',
|
||||
'books_sort_move_prev_chapter' => 'نقل إلى الفصل السابق',
|
||||
'books_sort_move_next_chapter' => 'نقل إلى الفصل التالي',
|
||||
'books_sort_move_book_start' => 'نقل إلى بداية الكتاب',
|
||||
'books_sort_move_book_end' => 'نقل إلى نهاية الكتاب',
|
||||
'books_sort_move_before_chapter' => 'نقل إلى الفصل السابق',
|
||||
'books_sort_move_after_chapter' => 'نقل إلى الفصل التالي',
|
||||
'books_copy' => 'نسخة الكتاب',
|
||||
'books_copy_success' => 'تم نسخ الكتاب بنجاح',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'فصل',
|
||||
@@ -198,21 +200,21 @@ return [
|
||||
'chapters_create' => 'إنشاء فصل جديد',
|
||||
'chapters_delete' => 'حذف الفصل',
|
||||
'chapters_delete_named' => 'حذف فصل :chapterName',
|
||||
'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
|
||||
'chapters_delete_explain' => 'سيؤدي هذا إلى حذف الفصل الذي يحمل الاسم \':chapterName\'. كما سيتم حذف جميع الصفحات الموجودة داخل هذا الفصل.',
|
||||
'chapters_delete_confirm' => 'تأكيد حذف الفصل؟',
|
||||
'chapters_edit' => 'تعديل الفصل',
|
||||
'chapters_edit_named' => 'تعديل فصل :chapterName',
|
||||
'chapters_save' => 'حفظ الفصل',
|
||||
'chapters_move' => 'نقل الفصل',
|
||||
'chapters_move_named' => 'نقل فصل :chapterName',
|
||||
'chapters_copy' => 'Copy Chapter',
|
||||
'chapters_copy_success' => 'Chapter successfully copied',
|
||||
'chapters_copy' => 'نسخ الفصل',
|
||||
'chapters_copy_success' => 'تم نسخ الفصل بنجاح',
|
||||
'chapters_permissions' => 'أذونات الفصل',
|
||||
'chapters_empty' => 'لا توجد أي صفحات في هذا الفصل حالياً',
|
||||
'chapters_permissions_active' => 'أذونات الفصل مفعلة',
|
||||
'chapters_permissions_success' => 'تم تحديث أذونات الفصل',
|
||||
'chapters_search_this' => 'البحث في هذا الفصل',
|
||||
'chapter_sort_book' => 'Sort Book',
|
||||
'chapter_sort_book' => 'فرز الكتاب',
|
||||
|
||||
// Pages
|
||||
'page' => 'صفحة',
|
||||
@@ -228,7 +230,7 @@ return [
|
||||
'pages_delete_draft' => 'حذف المسودة',
|
||||
'pages_delete_success' => 'تم حذف الصفحة',
|
||||
'pages_delete_draft_success' => 'تم حذف المسودة',
|
||||
'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',
|
||||
'pages_delete_warning_template' => 'هذه الصفحة قيد الاستخدام كقالب افتراضي لصفحات الكتب أو الفصول. لن يكون لهذه الكتب أو الفصول قالب افتراضي بعد حذفها.',
|
||||
'pages_delete_confirm' => 'تأكيد حذف الصفحة؟',
|
||||
'pages_delete_draft_confirm' => 'تأكيد حذف المسودة؟',
|
||||
'pages_editing_named' => ':pageName قيد التعديل',
|
||||
@@ -239,23 +241,23 @@ return [
|
||||
'pages_editing_page' => 'الصفحة قيد التعديل',
|
||||
'pages_edit_draft_save_at' => 'تم خفظ المسودة في ',
|
||||
'pages_edit_delete_draft' => 'حذف المسودة',
|
||||
'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',
|
||||
'pages_edit_delete_draft_confirm' => 'متيقِّن من رغبتك في حذف تغييرات صفحة المُسَوَّدَة؟ ستُفقد جميع تغييراتك، منذ آخر حفظ كامل، وسيتم تحديث المحرر بأحدث حالة حفظ للصفحة (غير مسودة).',
|
||||
'pages_edit_discard_draft' => 'التخلص من المسودة',
|
||||
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
|
||||
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
|
||||
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
|
||||
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
|
||||
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
|
||||
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
|
||||
'pages_edit_switch_to_markdown' => 'التبديل إلى محرر ماركداون -Markdown-',
|
||||
'pages_edit_switch_to_markdown_clean' => '(محتوى نظيف)',
|
||||
'pages_edit_switch_to_markdown_stable' => '(محتوى مستقر)',
|
||||
'pages_edit_switch_to_wysiwyg' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه -WYSIWYG-',
|
||||
'pages_edit_switch_to_new_wysiwyg' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه الجديد -new WYSIWYG-',
|
||||
'pages_edit_switch_to_new_wysiwyg_desc' => '(في اختبار ألف)',
|
||||
'pages_edit_set_changelog' => 'تثبيت سجل التعديل',
|
||||
'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت',
|
||||
'pages_edit_enter_changelog' => 'أدخل سجل التعديل',
|
||||
'pages_editor_switch_title' => 'Switch Editor',
|
||||
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
|
||||
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
|
||||
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
|
||||
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
|
||||
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
|
||||
'pages_editor_switch_title' => 'تبديل المحرر',
|
||||
'pages_editor_switch_are_you_sure' => 'متيقِّن أنك تريد تغيير المحرر لهذه الصفحة؟',
|
||||
'pages_editor_switch_consider_following' => 'عند تغيير المحررين، ضع في اعتبارك ما يلي:',
|
||||
'pages_editor_switch_consideration_a' => 'بمجرد الحفظ، سيتم استخدام خِيار المحرر الجديد بواسطة أي محررين مستقبليين، بما في ذلك أولئك الذين قد لا يتمكنون من تغيير نوع المحرر بأنفسهم.',
|
||||
'pages_editor_switch_consideration_b' => 'من الممكن أن يؤدي هذا إلى فقدان التفاصيل والنحو في ظروف معينة.',
|
||||
'pages_editor_switch_consideration_c' => 'لن تستمر تغييرات العلامة أو سجل التغييرات، التي تم إجراؤها منذ الحفظ الأخير، عبر هذا التغيير.',
|
||||
'pages_save' => 'حفظ الصفحة',
|
||||
'pages_title' => 'عنوان الصفحة',
|
||||
'pages_name' => 'اسم الصفحة',
|
||||
@@ -264,10 +266,10 @@ return [
|
||||
'pages_md_insert_image' => 'إدخال صورة',
|
||||
'pages_md_insert_link' => 'إدراج ارتباط الكيان',
|
||||
'pages_md_insert_drawing' => 'إدخال رسمة',
|
||||
'pages_md_show_preview' => 'Show preview',
|
||||
'pages_md_sync_scroll' => 'Sync preview scroll',
|
||||
'pages_drawing_unsaved' => 'Unsaved Drawing Found',
|
||||
'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
|
||||
'pages_md_show_preview' => 'عرض المعاينة',
|
||||
'pages_md_sync_scroll' => 'مزامنة معاينة التمرير',
|
||||
'pages_drawing_unsaved' => 'تم العثور على رسم غير محفوظ',
|
||||
'pages_drawing_unsaved_confirm' => 'تم العثور على بيانات رسم غير محفوظة من محاولة حفظ رسم سابقة فاشلة. هل ترغب في استعادة هذا الرسم غير المحفوظ ومواصلة تحريره؟',
|
||||
'pages_not_in_chapter' => 'صفحة ليست في فصل',
|
||||
'pages_move' => 'نقل الصفحة',
|
||||
'pages_copy' => 'نسخ الصفحة',
|
||||
@@ -277,17 +279,17 @@ return [
|
||||
'pages_permissions_success' => 'تم تحديث أذونات الصفحة',
|
||||
'pages_revision' => 'مراجعة',
|
||||
'pages_revisions' => 'مراجعات الصفحة',
|
||||
'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
|
||||
'pages_revisions_desc' => 'تجد أدناه جميع الإصدارات السابقة لهذه الصفحة. يمكنك الاطلاع عليها ومقارنتها واستعادة الإصدارات القديمة إذا سمحت الصلاحيات بذلك. قد لا يظهر تاريخ الصفحة بالكامل هنا، إذ قد تُحذف الإصدارات القديمة تلقائيًا، وذلك حسب إعدادات النظام.',
|
||||
'pages_revisions_named' => 'مراجعات صفحة :pageName',
|
||||
'pages_revision_named' => 'مراجعة صفحة :pageName',
|
||||
'pages_revision_restored_from' => 'Restored from #:id; :summary',
|
||||
'pages_revision_restored_from' => 'تم الاستعادة من #:id; :summary',
|
||||
'pages_revisions_created_by' => 'أنشئ بواسطة',
|
||||
'pages_revisions_date' => 'تاريخ المراجعة',
|
||||
'pages_revisions_number' => '#',
|
||||
'pages_revisions_sort_number' => 'Revision Number',
|
||||
'pages_revisions_sort_number' => 'رَقْم المراجعة',
|
||||
'pages_revisions_numbered' => 'مراجعة #:id',
|
||||
'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
|
||||
'pages_revisions_editor' => 'Editor Type',
|
||||
'pages_revisions_editor' => 'نوع المحرر',
|
||||
'pages_revisions_changelog' => 'سجل التعديل',
|
||||
'pages_revisions_changes' => 'التعديلات',
|
||||
'pages_revisions_current' => 'النسخة الحالية',
|
||||
@@ -295,20 +297,20 @@ return [
|
||||
'pages_revisions_restore' => 'استرجاع',
|
||||
'pages_revisions_none' => 'لا توجد مراجعات لهذه الصفحة',
|
||||
'pages_copy_link' => 'نسخ الرابط',
|
||||
'pages_edit_content_link' => 'Jump to section in editor',
|
||||
'pages_pointer_enter_mode' => 'Enter section select mode',
|
||||
'pages_pointer_label' => 'Page Section Options',
|
||||
'pages_pointer_permalink' => 'Page Section Permalink',
|
||||
'pages_pointer_include_tag' => 'Page Section Include Tag',
|
||||
'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',
|
||||
'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',
|
||||
'pages_edit_content_link' => 'انتقل إلى القسم في المحرر',
|
||||
'pages_pointer_enter_mode' => 'أدخل وضع اختيار القسم',
|
||||
'pages_pointer_label' => 'خيارات قسم الصفحة',
|
||||
'pages_pointer_permalink' => 'رابط دائم لقسم الصفحة',
|
||||
'pages_pointer_include_tag' => 'قسم الصفحة يتضمن العلامة',
|
||||
'pages_pointer_toggle_link' => 'وضع الرابط الدائم، اضغط لإظهار علامة التضمين',
|
||||
'pages_pointer_toggle_include' => 'تضمين وضع العلامة، اضغط لإظهار الرابط الدائم',
|
||||
'pages_permissions_active' => 'أذونات الصفحة مفعلة',
|
||||
'pages_initial_revision' => 'نشر مبدئي',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_references_update_revision' => 'التحديث التلقائي للنظام للروابط الداخلية',
|
||||
'pages_initial_name' => 'صفحة جديدة',
|
||||
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_page_changed_since_creation' => 'تم تحديث هذه الصفحة منذ إنشاء هذه المُسَوَّدَة. يُنصح بتجاهل هذه المُسَوَّدَة أو الحرص على عدم استبدال أي تغييرات في الصفحة.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
|
||||
'start_b' => ':userName بدأ بتعديل هذه الصفحة',
|
||||
@@ -316,44 +318,44 @@ return [
|
||||
'time_b' => 'في آخر :minCount دقيقة/دقائق',
|
||||
'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
|
||||
'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
|
||||
'pages_draft_discarded' => 'تم رفض المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.',
|
||||
'pages_draft_deleted' => 'تم حذف المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.',
|
||||
'pages_specific' => 'صفحة محددة',
|
||||
'pages_is_template' => 'قالب الصفحة',
|
||||
|
||||
// Editor Sidebar
|
||||
'toggle_sidebar' => 'Toggle Sidebar',
|
||||
'toggle_sidebar' => 'تبديل الشريط الجانبي',
|
||||
'page_tags' => 'وسوم الصفحة',
|
||||
'chapter_tags' => 'وسوم الفصل',
|
||||
'book_tags' => 'وسوم الكتاب',
|
||||
'shelf_tags' => 'علامات الرف',
|
||||
'tag' => 'وسم',
|
||||
'tags' => 'وسوم',
|
||||
'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
|
||||
'tags_index_desc' => 'يمكن تطبيق الوسوم على المحتوى داخل النظام لتطبيق تصنيف مرن. يمكن أن تحتوي الوسوم على مفتاح وقيمة، مع العلم أن القيمة اختيارية. بعد تطبيقها، يمكن الاستعلام عن المحتوى باستخدام اسم الوسم وقيمته.',
|
||||
'tag_name' => 'اسم العلامة',
|
||||
'tag_value' => 'قيمة الوسم (اختياري)',
|
||||
'tags_explain' => "إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.",
|
||||
'tags_add' => 'إضافة وسم آخر',
|
||||
'tags_remove' => 'إزالة هذه العلامة',
|
||||
'tags_usages' => 'Total tag usages',
|
||||
'tags_assigned_pages' => 'Assigned to Pages',
|
||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
||||
'tags_assigned_books' => 'Assigned to Books',
|
||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
||||
'tags_x_unique_values' => ':count unique values',
|
||||
'tags_all_values' => 'All values',
|
||||
'tags_view_tags' => 'View Tags',
|
||||
'tags_view_existing_tags' => 'View existing tags',
|
||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
||||
'tags_usages' => 'إجمالي استخدامات العلامة',
|
||||
'tags_assigned_pages' => 'مُخصصة للصفحات',
|
||||
'tags_assigned_chapters' => 'مُخصصة للفصول',
|
||||
'tags_assigned_books' => 'مُخصص للكتب',
|
||||
'tags_assigned_shelves' => 'مُخصصة للأرفف',
|
||||
'tags_x_unique_values' => 'قيم الفريدة :count',
|
||||
'tags_all_values' => 'جميع القيم',
|
||||
'tags_view_tags' => 'عرض العلامات',
|
||||
'tags_view_existing_tags' => 'عرض العلامات الموجودة',
|
||||
'tags_list_empty_hint' => 'يمكن تعيين العلامات بواسطة الشريط الجانبي لمحرر الصفحة أو خلال تحرير تفاصيل الكتاب أو الفصل أو الرف.',
|
||||
'attachments' => 'المرفقات',
|
||||
'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
|
||||
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا آنيا.',
|
||||
'attachments_upload' => 'رفع ملف',
|
||||
'attachments_link' => 'إرفاق رابط',
|
||||
'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
|
||||
'attachments_upload_drop' => 'وبدلاً من ذلك، يمكنك سحب المِلَفّ وإفلاته هنا لتحميله كمرفق.',
|
||||
'attachments_set_link' => 'تحديد الرابط',
|
||||
'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',
|
||||
'attachments_dropzone' => 'Drop files here to upload',
|
||||
'attachments_dropzone' => 'قم بإسقاط الملفات هنا للتحميل',
|
||||
'attachments_no_files' => 'لم تُرفع أي ملفات',
|
||||
'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
|
||||
'attachments_link_name' => 'اسم الرابط',
|
||||
@@ -396,13 +398,13 @@ return [
|
||||
'comment_new' => 'تعليق جديد',
|
||||
'comment_created' => 'تم التعليق :createDiff',
|
||||
'comment_updated' => 'تم التحديث :updateDiff بواسطة :username',
|
||||
'comment_updated_indicator' => 'Updated',
|
||||
'comment_updated_indicator' => 'تم التحديث',
|
||||
'comment_deleted_success' => 'تم حذف التعليق',
|
||||
'comment_created_success' => 'تمت إضافة التعليق',
|
||||
'comment_updated_success' => 'تم تحديث التعليق',
|
||||
'comment_delete_confirm' => 'تأكيد حذف التعليق؟',
|
||||
'comment_in_reply_to' => 'رداً على :commentId',
|
||||
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
|
||||
'comment_editor_explain' => 'هذه هي التعليقات المُضافة على هذه الصفحة. يُمكنك إضافة التعليقات وإدارتها عند عرض الصفحة المحفوظة.',
|
||||
|
||||
// Revision
|
||||
'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذه المراجعة؟',
|
||||
@@ -410,49 +412,49 @@ return [
|
||||
'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
|
||||
'copy_consider_owner' => 'You will become the owner of all copied content.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
'copy_consider' => 'يرجى مراعاة ما يلي عند نسخ المحتوى.',
|
||||
'copy_consider_permissions' => 'لن يتم نسخ إعدادات الصلاحيات المخصصة.',
|
||||
'copy_consider_owner' => 'سوف تصبح مالكًا لجميع المحتوى المنسوخ.',
|
||||
'copy_consider_images' => 'لن يتم تكرار ملفات صور الصفحة وستحتفظ الصور الأصلية بعلاقتها بالصفحة التي تم تحميلها إليها في الأصل.',
|
||||
'copy_consider_attachments' => 'لن يتم نسخ مرفقات الصفحة.',
|
||||
'copy_consider_access' => 'قد يؤدي تغيير الموقع أو المالك أو الصلاحيات إلى إمكانية وصول الأشخاص الذين لم يتمكنوا من الوصول إلى هذا المحتوى سابقًا.',
|
||||
|
||||
// Conversions
|
||||
'convert_to_shelf' => 'Convert to Shelf',
|
||||
'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',
|
||||
'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',
|
||||
'convert_book' => 'Convert Book',
|
||||
'convert_book_confirm' => 'Are you sure you want to convert this book?',
|
||||
'convert_undo_warning' => 'This cannot be as easily undone.',
|
||||
'convert_to_book' => 'Convert to Book',
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
'convert_to_shelf' => 'تحويل إلى رف',
|
||||
'convert_to_shelf_contents_desc' => 'يمكنك تحويل هذا الكتاب إلى رف جديد بنفس المحتويات. سيتم تحويل الفصول الموجودة فيه إلى كتب جديدة. إذا احتوى هذا الكتاب على أي صفحات غير موجودة في أي فصل، فسيتم إعادة تسمية الكتاب وإضافة هذه الصفحات إليه، وسيصبح جزءًا من الرف الجديد.',
|
||||
'convert_to_shelf_permissions_desc' => 'سيتم نسخ أي صلاحيات مُحددة لهذا الكتاب إلى الرف الجديد وإلى جميع الكتب الفرعية الجديدة التي لم تُطبّق عليها صلاحيات خاصة بها. يُرجى العلم بأن الصلاحيات على الرفوف لا تنتقل تلقائيًا إلى المحتوى داخلها، كما هو الحال مع الكتب.',
|
||||
'convert_book' => 'تحويل الكتاب',
|
||||
'convert_book_confirm' => 'هل أنت متيقِّن أنك تريد تحويل هذا الكتاب؟',
|
||||
'convert_undo_warning' => 'لا يمكن التراجع عن هذا الأمر بسهولة.',
|
||||
'convert_to_book' => 'تحويله إلى كتاب',
|
||||
'convert_to_book_desc' => 'يمكنك تحويل هذا الفصل إلى كتاب جديد بنفس المحتوى. سيتم نسخ أي صلاحيات مُعيّنة لهذا الفصل إلى الكتاب الجديد، ولكن لن يتم نسخ أي صلاحيات موروثة من الكتاب الأصلي، مما قد يؤدي إلى تغيير في التحكم في الوصول.',
|
||||
'convert_chapter' => 'تحويل الفصل',
|
||||
'convert_chapter_confirm' => 'هل أنت متيقِّن أنك تريد تحويل هذا الفصل؟',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',
|
||||
'references' => 'مراجع',
|
||||
'references_none' => 'لا توجد مراجع متعقبة لهذا العنصر.',
|
||||
'references_to_desc' => 'تجد أدناه كل المحتوى المعروف في النظام المرتبط بهذا العنصر.',
|
||||
|
||||
// Watch Options
|
||||
'watch' => 'Watch',
|
||||
'watch_title_default' => 'Default Preferences',
|
||||
'watch_desc_default' => 'Revert watching to just your default notification preferences.',
|
||||
'watch_title_ignore' => 'Ignore',
|
||||
'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',
|
||||
'watch_title_new' => 'New Pages',
|
||||
'watch_desc_new' => 'Notify when any new page is created within this item.',
|
||||
'watch_title_updates' => 'All Page Updates',
|
||||
'watch_desc_updates' => 'Notify upon all new pages and page changes.',
|
||||
'watch_desc_updates_page' => 'Notify upon all page changes.',
|
||||
'watch_title_comments' => 'All Page Updates & Comments',
|
||||
'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',
|
||||
'watch_desc_comments_page' => 'Notify upon page changes and new comments.',
|
||||
'watch_change_default' => 'Change default notification preferences',
|
||||
'watch_detail_ignore' => 'Ignoring notifications',
|
||||
'watch_detail_new' => 'Watching for new pages',
|
||||
'watch_detail_updates' => 'Watching new pages and updates',
|
||||
'watch_detail_comments' => 'Watching new pages, updates & comments',
|
||||
'watch' => 'شاهد',
|
||||
'watch_title_default' => 'التفضيلات الافتراضية',
|
||||
'watch_desc_default' => 'استعادة المشاهدة إلى تفضيلات الإشعارات الافتراضية فقط.',
|
||||
'watch_title_ignore' => 'تجاهل',
|
||||
'watch_desc_ignore' => 'تجاهل كافة الإشعارات، بما في ذلك تلك الواردة من تفضيلات مستوى المستخدم.',
|
||||
'watch_title_new' => 'صفحات جديدة',
|
||||
'watch_desc_new' => 'إعلام عند إنشاء أي صفحة جديدة ضمن هذا العنصر.',
|
||||
'watch_title_updates' => 'جميع تحديثات الصفحة',
|
||||
'watch_desc_updates' => 'إشعار بجميع الصفحات الجديدة والتغييرات في الصفحات.',
|
||||
'watch_desc_updates_page' => 'إشعار عند حدوث أي تغييرات في الصفحة.',
|
||||
'watch_title_comments' => 'جميع تحديثات الصفحة والتعليقات',
|
||||
'watch_desc_comments' => 'إشعار بجميع الصفحات الجديدة، وتغييرات الصفحات والتعليقات الجديدة.',
|
||||
'watch_desc_comments_page' => 'إشعار عند حدوث تغييرات في الصفحة أو تعليقات جديدة.',
|
||||
'watch_change_default' => 'تغيير تفضيلات الإشعارات الافتراضية',
|
||||
'watch_detail_ignore' => 'تجاهل الإشعارات',
|
||||
'watch_detail_new' => 'ترقب الصفحات الجديدة',
|
||||
'watch_detail_updates' => 'مشاهدة الصفحات الجديدة والتحديثات',
|
||||
'watch_detail_comments' => 'مشاهدة الصفحات الجديدة والتحديثات والتعليقات',
|
||||
'watch_detail_parent_book' => 'Watching via parent book',
|
||||
'watch_detail_parent_book_ignore' => 'Ignoring via parent book',
|
||||
'watch_detail_parent_chapter' => 'Watching via parent chapter',
|
||||
|
||||
@@ -74,6 +74,36 @@ return [
|
||||
'reg_confirm_restrict_domain_desc' => 'أدخل قائمة مفصولة بفواصل لنطاقات البريد الإلكتروني التي ترغب في تقييد التسجيل إليها. سيتم إرسال بريد إلكتروني للمستخدمين لتأكيد عنوانهم قبل السماح لهم بالتفاعل مع التطبيق. <br> لاحظ أن المستخدمين سيكونون قادرين على تغيير عناوين البريد الإلكتروني الخاصة بهم بعد التسجيل بنجاح.',
|
||||
'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
|
||||
'maint' => 'الصيانة',
|
||||
'maint_image_cleanup' => 'تنظيف الصور',
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'page_restore' => 'възстановена страница',
|
||||
'page_restore_notification' => 'Страницата е възстановена успешно',
|
||||
'page_move' => 'преместена страница',
|
||||
'page_move_notification' => 'Page successfully moved',
|
||||
'page_move_notification' => 'Страницата беше успешно преместена',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'създадена глава',
|
||||
@@ -25,13 +25,13 @@ return [
|
||||
'chapter_delete' => 'изтрита глава',
|
||||
'chapter_delete_notification' => 'Успешно изтрита глава',
|
||||
'chapter_move' => 'преместена глава',
|
||||
'chapter_move_notification' => 'Chapter successfully moved',
|
||||
'chapter_move_notification' => 'Главата е успешно преместена',
|
||||
|
||||
// Books
|
||||
'book_create' => 'създадена книга',
|
||||
'book_create_notification' => 'Книгата е създадена успешно',
|
||||
'book_create_from_chapter' => 'превърната глава в книга',
|
||||
'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',
|
||||
'book_create_from_chapter_notification' => 'Главата е успешно преобразувана в книга',
|
||||
'book_update' => 'обновена книга',
|
||||
'book_update_notification' => 'Книгата е обновена успешно',
|
||||
'book_delete' => 'изтрита книга',
|
||||
@@ -127,6 +127,14 @@ return [
|
||||
'comment_update' => 'updated 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
|
||||
'permissions_update' => 'обновени права',
|
||||
];
|
||||
|
||||
@@ -13,6 +13,7 @@ return [
|
||||
'cancel' => 'Отказ',
|
||||
'save' => 'Запис',
|
||||
'close' => 'Затваряне',
|
||||
'apply' => 'Apply',
|
||||
'undo' => 'Отмяна',
|
||||
'redo' => 'Повтаряне',
|
||||
'left' => 'Вляво',
|
||||
@@ -147,6 +148,7 @@ return [
|
||||
'url' => 'URL',
|
||||
'text_to_display' => 'Текст за показване',
|
||||
'title' => 'Заглавие',
|
||||
'browse_links' => 'Browse links',
|
||||
'open_link' => 'Open link',
|
||||
'open_link_in' => 'Open link in...',
|
||||
'open_link_current' => 'Текущ прозорец',
|
||||
|
||||
@@ -166,7 +166,9 @@ return [
|
||||
'books_search_this' => 'Търси в книгата',
|
||||
'books_navigation' => 'Навигация на книгата',
|
||||
'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_name' => 'Сортиране по име',
|
||||
'books_sort_created' => 'Сортирай по дата на създаване',
|
||||
|
||||
@@ -74,6 +74,36 @@ return [
|
||||
'reg_confirm_restrict_domain_desc' => 'Въведи разделен със запетаи списък от имейл домейни, до които да бъде ограничена регистрацията. На потребителите ще им бъде изпратен имейл, за да потвърдят адреса, преди да могат да използват приложението. <br> Имай предвид, че потребителите ще могат да сменят имейл адресите си след успешна регистрация.',
|
||||
'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
|
||||
'maint' => 'Поддръжка',
|
||||
'maint_image_cleanup' => 'Разчисти изображения',
|
||||
|
||||
@@ -127,6 +127,14 @@ return [
|
||||
'comment_update' => 'মন্তব্য হালনাগাদ করেছেন',
|
||||
'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
|
||||
'permissions_update' => 'অনুমতিক্রম হালনাগাদ করেছেন',
|
||||
];
|
||||
|
||||
@@ -14,16 +14,16 @@ return [
|
||||
'log_in' => 'লগ ইন করুন',
|
||||
'log_in_with' => ':socialDriver দ্বারা লগইন করুন',
|
||||
'sign_up_with' => ':socialDriver দ্বারা নিবন্ধিত হোন',
|
||||
'logout' => 'Logout',
|
||||
'logout' => 'লগআউট',
|
||||
|
||||
'name' => 'Name',
|
||||
'username' => 'Username',
|
||||
'email' => 'Email',
|
||||
'password' => 'Password',
|
||||
'password_confirm' => 'Confirm Password',
|
||||
'password_hint' => 'Must be at least 8 characters',
|
||||
'forgot_password' => 'Forgot Password?',
|
||||
'remember_me' => 'Remember Me',
|
||||
'name' => 'নাম',
|
||||
'username' => 'ব্যবহারকারী',
|
||||
'email' => 'ই-মেইল',
|
||||
'password' => 'পাসওয়ার্ড',
|
||||
'password_confirm' => 'পাসওয়ার্ডের পুনরাবৃত্তি',
|
||||
'password_hint' => 'ন্যূনতম ৮ অক্ষরের হতে হবে',
|
||||
'forgot_password' => 'পাসওয়ার্ড ভুলে গেছেন?',
|
||||
'remember_me' => 'লগইন স্থায়িত্ব ধরে রাখুন',
|
||||
'ldap_email_hint' => 'Please enter an email to use for this account.',
|
||||
'create_account' => 'Create Account',
|
||||
'already_have_account' => 'Already have an account?',
|
||||
|
||||
@@ -13,6 +13,7 @@ return [
|
||||
'cancel' => 'প্রত্যাহার করুন',
|
||||
'save' => 'সংরক্ষণ করুন',
|
||||
'close' => 'বন্ধ করুন',
|
||||
'apply' => 'Apply',
|
||||
'undo' => 'প্রত্যাহার করুন',
|
||||
'redo' => 'পুনর্বহাল রাখুন',
|
||||
'left' => 'বাম',
|
||||
@@ -147,6 +148,7 @@ return [
|
||||
'url' => 'URL',
|
||||
'text_to_display' => 'Text to display',
|
||||
'title' => 'Title',
|
||||
'browse_links' => 'Browse links',
|
||||
'open_link' => 'Open link',
|
||||
'open_link_in' => 'Open link in...',
|
||||
'open_link_current' => 'Current window',
|
||||
|
||||
@@ -166,7 +166,9 @@ return [
|
||||
'books_search_this' => 'Search this book',
|
||||
'books_navigation' => 'Book Navigation',
|
||||
'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_name' => 'Sort by Name',
|
||||
'books_sort_created' => 'Sort by Created Date',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user