From b6110ed3cd15cf9d0472f1ccced3dbe6fdc7db76 Mon Sep 17 00:00:00 2001 From: Joshua Booth Date: Tue, 13 May 2025 20:17:36 +0100 Subject: [PATCH 001/139] Add settings for number of books/shelves that will be displayed per page --- app/App/HomeController.php | 4 +-- app/Entities/Controllers/BookController.php | 2 +- .../Controllers/BookshelfController.php | 2 +- lang/en/settings.php | 4 +++ .../settings/categories/sorting.blade.php | 34 +++++++++++++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/App/HomeController.php b/app/App/HomeController.php index 0585e0af5..7ec8b0af2 100644 --- a/app/App/HomeController.php +++ b/app/App/HomeController.php @@ -83,7 +83,7 @@ class HomeController extends Controller if ($homepageOption === 'bookshelves') { $shelves = $this->queries->shelves->visibleForListWithCover() ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder()) - ->paginate(18); + ->paginate(intval(setting('sorting-shelves-per-page', '18'))); $data = array_merge($commonData, ['shelves' => $shelves]); return view('home.shelves', $data); @@ -92,7 +92,7 @@ class HomeController extends Controller if ($homepageOption === 'books') { $books = $this->queries->books->visibleForListWithCover() ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder()) - ->paginate(18); + ->paginate(intval(setting('sorting-books-per-page', '18'))); $data = array_merge($commonData, ['books' => $books]); return view('home.books', $data); diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index b1685081a..a9f0d34dc 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -48,7 +48,7 @@ class BookController extends Controller $books = $this->queries->visibleForListWithCover() ->orderBy($listOptions->getSort(), $listOptions->getOrder()) - ->paginate(18); + ->paginate(intval(setting('sorting-books-per-page', '18'))); $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false; $popular = $this->queries->popularForList()->take(4)->get(); $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get(); diff --git a/app/Entities/Controllers/BookshelfController.php b/app/Entities/Controllers/BookshelfController.php index 6cedd23e7..da84cd447 100644 --- a/app/Entities/Controllers/BookshelfController.php +++ b/app/Entities/Controllers/BookshelfController.php @@ -42,7 +42,7 @@ class BookshelfController extends Controller $shelves = $this->queries->visibleForListWithCover() ->orderBy($listOptions->getSort(), $listOptions->getOrder()) - ->paginate(18); + ->paginate(intval(setting('sorting-shelves-per-page', '18'))); $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false; $popular = $this->queries->popularForList()->get(); $new = $this->queries->visibleForList() diff --git a/lang/en/settings.php b/lang/en/settings.php index 82a4ade5d..9e49197c0 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -76,6 +76,10 @@ return [ // Sorting Settings 'sorting' => 'Sorting', + 'sorting_shelves_per_page' => 'Shelves Per Page', + 'sorting_shelves_per_page_desc' => 'Sets the number of shelves shown per page in shelf listings.', + 'sorting_books_per_page' => 'Books Per Page', + 'sorting_books_per_page_desc' => 'Sets the number of books shown per page in book listings.', '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', diff --git a/resources/views/settings/categories/sorting.blade.php b/resources/views/settings/categories/sorting.blade.php index 9d1d9814b..4aff11bd2 100644 --- a/resources/views/settings/categories/sorting.blade.php +++ b/resources/views/settings/categories/sorting.blade.php @@ -10,6 +10,40 @@ {{ csrf_field() }} +
+
+ +

{{ trans('settings.sorting_shelves_per_page_desc') }}

+
+
+ +
+
+ +
+
+ +

{{ trans('settings.sorting_books_per_page_desc') }}

+
+
+ +
+
+
From a961552c23101bcf260aaf930a8362277852f67b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 5 Aug 2025 13:39:30 +0100 Subject: [PATCH 002/139] Commands: Updated create admin comment to accept extra flags Added flags to target changes to the first default admin user, and to generate a password. This is related to #4575. --- app/Console/Commands/CreateAdminCommand.php | 93 ++++++++++++++++----- app/Users/UserRepo.php | 19 ++++- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/app/Console/Commands/CreateAdminCommand.php b/app/Console/Commands/CreateAdminCommand.php index 82b6e50aa..ce619e05d 100644 --- a/app/Console/Commands/CreateAdminCommand.php +++ b/app/Console/Commands/CreateAdminCommand.php @@ -21,7 +21,9 @@ class CreateAdminCommand extends Command {--email= : The email address for the new admin user} {--name= : The name of the new admin user} {--password= : The password to assign to the new admin user} - {--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}'; + {--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)} + {--generate-password : Generate a random password for the new admin user} + {--first-admin : Indicate if this should set/update the details of the initial admin user}'; /** * The console command description. @@ -35,23 +37,9 @@ class CreateAdminCommand extends Command */ public function handle(UserRepo $userRepo): int { - $details = $this->snakeCaseOptions(); - - if (empty($details['email'])) { - $details['email'] = $this->ask('Please specify an email address for the new admin user'); - } - - if (empty($details['name'])) { - $details['name'] = $this->ask('Please specify a name for the new admin user'); - } - - if (empty($details['password'])) { - if (empty($details['external_auth_id'])) { - $details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)'); - } else { - $details['password'] = Str::random(32); - } - } + $firstAdminOnly = $this->option('first-admin'); + $shouldGeneratePassword = $this->option('generate-password'); + $details = $this->gatherDetails($shouldGeneratePassword); $validator = Validator::make($details, [ 'email' => ['required', 'email', 'min:5', new Unique('users', 'email')], @@ -68,16 +56,81 @@ class CreateAdminCommand extends Command return 1; } + $adminRole = Role::getSystemRole('admin'); + + if ($firstAdminOnly) { + $handled = $this->handleFirstAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole); + if ($handled) { + return 0; + } + } + $user = $userRepo->createWithoutActivity($validator->validated()); - $user->attachRole(Role::getSystemRole('admin')); + $user->attachRole($adminRole); $user->email_confirmed = true; $user->save(); - $this->info("Admin account with email \"{$user->email}\" successfully created!"); + if ($shouldGeneratePassword) { + $this->line($details['password']); + } else { + $this->info("Admin account with email \"{$user->email}\" successfully created!"); + } return 0; } + /** + * Handle updates to the first admin if exists. + * Returns true if the action has been handled (user updated or already a non-default admin user) otherwise + * returns false if no action has been taken, and we therefore need to proceed with a normal account creation. + */ + protected function handleFirstAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): bool + { + $defaultAdmin = $userRepo->getByEmail('admin@admin.com'); + if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) { + $userRepo->updateWithoutActivity($defaultAdmin, $data, true); + if ($generatePassword) { + $this->line($data['password']); + } else { + $this->info("The default admin user has been updated with the provided details!"); + } + + return true; + } else if ($adminRole->users()->count() > 0) { + $this->warn('Non-default admin user already exists. Skipping creation of new admin user.'); + return true; + } + + return false; + } + + protected function gatherDetails(bool $generatePassword): array + { + $details = $this->snakeCaseOptions(); + + if (empty($details['email'])) { + $details['email'] = $this->ask('Please specify an email address for the new admin user'); + } + + if (empty($details['name'])) { + $details['name'] = $this->ask('Please specify a name for the new admin user'); + } + + if (empty($details['password'])) { + if (empty($details['external_auth_id'])) { + if ($generatePassword) { + $details['password'] = Str::random(32); + } else { + $details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)'); + } + } else { + $details['password'] = Str::random(32); + } + } + + return $details; + } + protected function snakeCaseOptions(): array { $returnOpts = []; diff --git a/app/Users/UserRepo.php b/app/Users/UserRepo.php index 5c8ace8fa..d24f7002e 100644 --- a/app/Users/UserRepo.php +++ b/app/Users/UserRepo.php @@ -100,13 +100,13 @@ class UserRepo } /** - * Update the given user with the given data. + * Update the given user with the given data, but do not create an activity. * * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array, language: ?string} $data * * @throws UserUpdateException */ - public function update(User $user, array $data, bool $manageUsersAllowed): User + public function updateWithoutActivity(User $user, array $data, bool $manageUsersAllowed): User { if (!empty($data['name'])) { $user->name = $data['name']; @@ -134,6 +134,21 @@ class UserRepo } $user->save(); + + return $user; + } + + /** + * Update the given user with the given data. + * + * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array, language: ?string} $data + * + * @throws UserUpdateException + */ + public function update(User $user, array $data, bool $manageUsersAllowed): User + { + $user = $this->updateWithoutActivity($user, $data, $manageUsersAllowed); + Activity::add(ActivityType::USER_UPDATE, $user); return $user; From 2eefbd21c1cdb6a3b32529fd6db10f9bbfd4b9da Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 5 Aug 2025 16:43:06 +0100 Subject: [PATCH 003/139] Commands: Added testing for initial admin changes - Also changed first-admin to initial. - Updated initial handling to not require email/name to be passed, using defaults instead. - Adds missing existing email use check. --- app/Console/Commands/CreateAdminCommand.php | 53 ++++-- tests/Commands/CreateAdminCommandTest.php | 185 +++++++++++++++++++- 2 files changed, 212 insertions(+), 26 deletions(-) diff --git a/app/Console/Commands/CreateAdminCommand.php b/app/Console/Commands/CreateAdminCommand.php index ce619e05d..520f0822a 100644 --- a/app/Console/Commands/CreateAdminCommand.php +++ b/app/Console/Commands/CreateAdminCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Validation\Rules\Password; -use Illuminate\Validation\Rules\Unique; class CreateAdminCommand extends Command { @@ -23,7 +22,7 @@ class CreateAdminCommand extends Command {--password= : The password to assign to the new admin user} {--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)} {--generate-password : Generate a random password for the new admin user} - {--first-admin : Indicate if this should set/update the details of the initial admin user}'; + {--initial : Indicate if this should set/update the details of the initial admin user}'; /** * The console command description. @@ -37,12 +36,12 @@ class CreateAdminCommand extends Command */ public function handle(UserRepo $userRepo): int { - $firstAdminOnly = $this->option('first-admin'); + $initialAdminOnly = $this->option('initial'); $shouldGeneratePassword = $this->option('generate-password'); - $details = $this->gatherDetails($shouldGeneratePassword); + $details = $this->gatherDetails($shouldGeneratePassword, $initialAdminOnly); $validator = Validator::make($details, [ - 'email' => ['required', 'email', 'min:5', new Unique('users', 'email')], + 'email' => ['required', 'email', 'min:5'], 'name' => ['required', 'min:2'], 'password' => ['required_without:external_auth_id', Password::default()], 'external_auth_id' => ['required_without:password'], @@ -58,13 +57,20 @@ class CreateAdminCommand extends Command $adminRole = Role::getSystemRole('admin'); - if ($firstAdminOnly) { - $handled = $this->handleFirstAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole); - if ($handled) { - return 0; + if ($initialAdminOnly) { + $handled = $this->handleInitialAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole); + if ($handled !== null) { + return $handled ? 0 : 1; } } + $emailUsed = $userRepo->getByEmail($details['email']) !== null; + if ($emailUsed) { + $this->error("Could not create admin account."); + $this->error("An account with the email address \"{$details['email']}\" already exists."); + return 1; + } + $user = $userRepo->createWithoutActivity($validator->validated()); $user->attachRole($adminRole); $user->email_confirmed = true; @@ -80,14 +86,19 @@ class CreateAdminCommand extends Command } /** - * Handle updates to the first admin if exists. - * Returns true if the action has been handled (user updated or already a non-default admin user) otherwise - * returns false if no action has been taken, and we therefore need to proceed with a normal account creation. + * Handle updates to the original admin account if it exists. + * Returns true if it's been successfully handled, false if unsuccessful, or null if not handled. */ - protected function handleFirstAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): bool + protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): bool|null { $defaultAdmin = $userRepo->getByEmail('admin@admin.com'); if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) { + if ($defaultAdmin->email !== $data['email'] && $userRepo->getByEmail($data['email']) !== null) { + $this->error("Could not create admin account."); + $this->error("An account with the email address \"{$data['email']}\" already exists."); + return false; + } + $userRepo->updateWithoutActivity($defaultAdmin, $data, true); if ($generatePassword) { $this->line($data['password']); @@ -101,19 +112,27 @@ class CreateAdminCommand extends Command return true; } - return false; + return null; } - protected function gatherDetails(bool $generatePassword): array + protected function gatherDetails(bool $generatePassword, bool $initialAdmin): array { $details = $this->snakeCaseOptions(); if (empty($details['email'])) { - $details['email'] = $this->ask('Please specify an email address for the new admin user'); + if ($initialAdmin) { + $details['email'] = 'admin@example.com'; + } else { + $details['email'] = $this->ask('Please specify an email address for the new admin user'); + } } if (empty($details['name'])) { - $details['name'] = $this->ask('Please specify a name for the new admin user'); + if ($initialAdmin) { + $details['name'] = 'Admin'; + } else { + $details['name'] = $this->ask('Please specify a name for the new admin user'); + } } if (empty($details['password'])) { diff --git a/tests/Commands/CreateAdminCommandTest.php b/tests/Commands/CreateAdminCommandTest.php index 95a39c497..fa1329ad3 100644 --- a/tests/Commands/CreateAdminCommandTest.php +++ b/tests/Commands/CreateAdminCommandTest.php @@ -2,8 +2,11 @@ namespace Tests\Commands; +use BookStack\Users\Models\Role; use BookStack\Users\Models\User; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Tests\TestCase; class CreateAdminCommandTest extends TestCase @@ -11,14 +14,14 @@ class CreateAdminCommandTest extends TestCase public function test_standard_command_usage() { $this->artisan('bookstack:create-admin', [ - '--email' => 'admintest@example.com', - '--name' => 'Admin Test', + '--email' => 'admintest@example.com', + '--name' => 'Admin Test', '--password' => 'testing-4', ])->assertExitCode(0); $this->assertDatabaseHas('users', [ 'email' => 'admintest@example.com', - 'name' => 'Admin Test', + 'name' => 'Admin Test', ]); /** @var User $user */ @@ -30,14 +33,14 @@ class CreateAdminCommandTest extends TestCase public function test_providing_external_auth_id() { $this->artisan('bookstack:create-admin', [ - '--email' => 'admintest@example.com', - '--name' => 'Admin Test', + '--email' => 'admintest@example.com', + '--name' => 'Admin Test', '--external-auth-id' => 'xX_admin_Xx', ])->assertExitCode(0); $this->assertDatabaseHas('users', [ - 'email' => 'admintest@example.com', - 'name' => 'Admin Test', + 'email' => 'admintest@example.com', + 'name' => 'Admin Test', 'external_auth_id' => 'xX_admin_Xx', ]); @@ -50,14 +53,178 @@ class CreateAdminCommandTest extends TestCase { $this->artisan('bookstack:create-admin', [ '--email' => 'admintest@example.com', - '--name' => 'Admin Test', + '--name' => 'Admin Test', ])->expectsQuestion('Please specify a password for the new admin user (8 characters min)', 'hunter2000') ->assertExitCode(0); $this->assertDatabaseHas('users', [ 'email' => 'admintest@example.com', - 'name' => 'Admin Test', + 'name' => 'Admin Test', ]); $this->assertTrue(Auth::attempt(['email' => 'admintest@example.com', 'password' => 'hunter2000'])); } + + public function test_generate_password_option() + { + $this->withoutMockingConsoleOutput() + ->artisan('bookstack:create-admin', [ + '--email' => 'admintest@example.com', + '--name' => 'Admin Test', + '--generate-password' => true, + ]); + + $output = trim(Artisan::output()); + $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{32}$/', $output); + + $user = User::query()->where('email', '=', 'admintest@example.com')->first(); + $this->assertTrue(Hash::check($output, $user->password)); + } + + public function test_initial_option_updates_default_admin() + { + $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first(); + + $this->artisan('bookstack:create-admin', [ + '--email' => 'firstadmin@example.com', + '--name' => 'Admin Test', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput('The default admin user has been updated with the provided details!') + ->assertExitCode(0); + + $defaultAdmin->refresh(); + + $this->assertEquals('firstadmin@example.com', $defaultAdmin->email); + } + + public function test_initial_option_does_not_update_if_only_non_default_admin_exists() + { + $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first(); + $defaultAdmin->email = 'testadmin@example.com'; + $defaultAdmin->save(); + + $this->artisan('bookstack:create-admin', [ + '--email' => 'firstadmin@example.com', + '--name' => 'Admin Test', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput('Non-default admin user already exists. Skipping creation of new admin user.') + ->assertExitCode(0); + + $defaultAdmin->refresh(); + + $this->assertEquals('testadmin@example.com', $defaultAdmin->email); + } + + public function test_initial_option_updates_creates_new_admin_if_none_exists() + { + $adminRole = Role::getSystemRole('admin'); + $adminRole->users()->delete(); + $this->assertEquals(0, $adminRole->users()->count()); + + $this->artisan('bookstack:create-admin', [ + '--email' => 'firstadmin@example.com', + '--name' => 'My initial admin', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput("Admin account with email \"firstadmin@example.com\" successfully created!") + ->assertExitCode(0); + + $this->assertEquals(1, $adminRole->users()->count()); + $this->assertDatabaseHas('users', [ + 'email' => 'firstadmin@example.com', + 'name' => 'My initial admin', + ]); + } + + public function test_initial_rerun_does_not_error_but_skips() + { + $adminRole = Role::getSystemRole('admin'); + $adminRole->users()->delete(); + + $this->artisan('bookstack:create-admin', [ + '--email' => 'firstadmin@example.com', + '--name' => 'My initial admin', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput("Admin account with email \"firstadmin@example.com\" successfully created!") + ->assertExitCode(0); + + $this->artisan('bookstack:create-admin', [ + '--email' => 'firstadmin@example.com', + '--name' => 'My initial admin', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput("Non-default admin user already exists. Skipping creation of new admin user.") + ->assertExitCode(0); + } + + public function test_initial_option_creation_errors_if_email_already_exists() + { + $adminRole = Role::getSystemRole('admin'); + $adminRole->users()->delete(); + $editor = $this->users->editor(); + + $this->artisan('bookstack:create-admin', [ + '--email' => $editor->email, + '--name' => 'My initial admin', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput("Could not create admin account.") + ->expectsOutput("An account with the email address \"{$editor->email}\" already exists.") + ->assertExitCode(1); + } + + public function test_initial_option_updating_errors_if_email_already_exists() + { + $editor = $this->users->editor(); + $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first(); + $this->assertNotNull($defaultAdmin); + + $this->artisan('bookstack:create-admin', [ + '--email' => $editor->email, + '--name' => 'My initial admin', + '--password' => 'testing-7', + '--initial' => true, + ])->expectsOutput("Could not create admin account.") + ->expectsOutput("An account with the email address \"{$editor->email}\" already exists.") + ->assertExitCode(1); + } + + public function test_initial_option_does_not_require_name_or_email_to_be_passed() + { + $adminRole = Role::getSystemRole('admin'); + $adminRole->users()->delete(); + $this->assertEquals(0, $adminRole->users()->count()); + + $this->artisan('bookstack:create-admin', [ + '--generate-password' => true, + '--initial' => true, + ])->assertExitCode(0); + + $this->assertEquals(1, $adminRole->users()->count()); + $this->assertDatabaseHas('users', [ + 'email' => 'admin@example.com', + 'name' => 'Admin', + ]); + } + + public function test_initial_option_updating_existing_user_with_generate_password_only_outputs_password() + { + $defaultAdmin = User::query()->where('email', '=', 'admin@admin.com')->first(); + + $this->withoutMockingConsoleOutput() + ->artisan('bookstack:create-admin', [ + '--email' => 'firstadmin@example.com', + '--name' => 'Admin Test', + '--generate-password' => true, + '--initial' => true, + ]); + + $output = Artisan::output(); + $this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{32}$/', $output); + + $defaultAdmin->refresh(); + $this->assertEquals('firstadmin@example.com', $defaultAdmin->email); + } } From f36e6fb929d50a2713d1b01f064f2f087188bb69 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 7 Aug 2025 13:16:49 +0100 Subject: [PATCH 004/139] Commands: Updated create admin skip return Return status for skipped --initial creation will now return 2, so that it can be identified seperate from a creation and from an error. --- app/Console/Commands/CreateAdminCommand.php | 12 ++++++------ tests/Commands/CreateAdminCommandTest.php | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Console/Commands/CreateAdminCommand.php b/app/Console/Commands/CreateAdminCommand.php index 520f0822a..bf72553f7 100644 --- a/app/Console/Commands/CreateAdminCommand.php +++ b/app/Console/Commands/CreateAdminCommand.php @@ -60,7 +60,7 @@ class CreateAdminCommand extends Command if ($initialAdminOnly) { $handled = $this->handleInitialAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole); if ($handled !== null) { - return $handled ? 0 : 1; + return $handled; } } @@ -87,16 +87,16 @@ class CreateAdminCommand extends Command /** * Handle updates to the original admin account if it exists. - * Returns true if it's been successfully handled, false if unsuccessful, or null if not handled. + * Returns an int return status if handled, otherwise returns null if not handled (new user to be created). */ - protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): bool|null + protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): int|null { $defaultAdmin = $userRepo->getByEmail('admin@admin.com'); if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) { if ($defaultAdmin->email !== $data['email'] && $userRepo->getByEmail($data['email']) !== null) { $this->error("Could not create admin account."); $this->error("An account with the email address \"{$data['email']}\" already exists."); - return false; + return 1; } $userRepo->updateWithoutActivity($defaultAdmin, $data, true); @@ -106,10 +106,10 @@ class CreateAdminCommand extends Command $this->info("The default admin user has been updated with the provided details!"); } - return true; + return 0; } else if ($adminRole->users()->count() > 0) { $this->warn('Non-default admin user already exists. Skipping creation of new admin user.'); - return true; + return 2; } return null; diff --git a/tests/Commands/CreateAdminCommandTest.php b/tests/Commands/CreateAdminCommandTest.php index fa1329ad3..f389dd942 100644 --- a/tests/Commands/CreateAdminCommandTest.php +++ b/tests/Commands/CreateAdminCommandTest.php @@ -109,7 +109,7 @@ class CreateAdminCommandTest extends TestCase '--password' => 'testing-7', '--initial' => true, ])->expectsOutput('Non-default admin user already exists. Skipping creation of new admin user.') - ->assertExitCode(0); + ->assertExitCode(2); $defaultAdmin->refresh(); @@ -156,7 +156,7 @@ class CreateAdminCommandTest extends TestCase '--password' => 'testing-7', '--initial' => true, ])->expectsOutput("Non-default admin user already exists. Skipping creation of new admin user.") - ->assertExitCode(0); + ->assertExitCode(2); } public function test_initial_option_creation_errors_if_email_already_exists() From 7c79b10fb6520f5bbd84e7de1369775d78ccadc3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 24 Aug 2025 14:02:21 +0100 Subject: [PATCH 005/139] Imports: Fixed drawing IDs not being updated in content Would leave imported content with inaccessible images in many cases (or wrong references) although the drawing was still being uploaded & related to the page. Added test to cover. For #5761 --- .../ZipExports/ZipImportReferences.php | 23 ++++++++++- tests/Exports/ZipImportRunnerTest.php | 38 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/Exports/ZipExports/ZipImportReferences.php b/app/Exports/ZipExports/ZipImportReferences.php index da0581df6..662dfdf6c 100644 --- a/app/Exports/ZipExports/ZipImportReferences.php +++ b/app/Exports/ZipExports/ZipImportReferences.php @@ -29,7 +29,10 @@ class ZipImportReferences /** @var Image[] */ protected array $images = []; - /** @var array */ + /** + * Mapping keyed by "type:old-reference-id" with values being the new imported equivalent model. + * @var array + */ protected array $referenceMap = []; /** @var array */ @@ -108,6 +111,22 @@ class ZipImportReferences return null; } + protected function replaceDrawingIdReferences(string $content): string + { + $referenceRegex = '/\sdrawio-diagram=[\'"](\d+)[\'"]/'; + + $result = preg_replace_callback($referenceRegex, function ($matches) { + $key = 'image:' . $matches[1]; + $model = $this->referenceMap[$key] ?? null; + if ($model instanceof Image && $model->type === 'drawio') { + return ' drawio-diagram="' . $model->id . '"'; + } + return $matches[0]; + }, $content); + + return $result ?: $content; + } + public function replaceReferences(): void { foreach ($this->books as $book) { @@ -134,7 +153,9 @@ class ZipImportReferences $exportPage = $this->zipExportPageMap[$page->id]; $contentType = $exportPage->markdown ? 'markdown' : 'html'; $content = $exportPage->markdown ?: ($exportPage->html ?: ''); + $parsed = $this->parser->parseReferences($content, $this->handleReference(...)); + $parsed = $this->replaceDrawingIdReferences($parsed); $this->pageRepo->setContentFromInput($page, [ $contentType => $parsed, diff --git a/tests/Exports/ZipImportRunnerTest.php b/tests/Exports/ZipImportRunnerTest.php index d3af6df76..68ffb4231 100644 --- a/tests/Exports/ZipImportRunnerTest.php +++ b/tests/Exports/ZipImportRunnerTest.php @@ -393,4 +393,42 @@ class ZipImportRunnerTest extends TestCase ZipTestHelper::deleteZipForImport($import); } + + public function test_drawing_references_are_updated_within_content() + { + $testImagePath = $this->files->testFilePath('test-image.png'); + $parent = $this->entities->chapter(); + + $import = ZipTestHelper::importFromData([], [ + 'page' => [ + 'name' => 'Page A', + 'html' => '
', + 'images' => [ + [ + 'id' => 1125, + 'name' => 'Cat', + 'type' => 'drawio', + 'file' => 'my_drawing' + ] + ], + ], + ], [ + 'my_drawing' => $testImagePath, + ]); + + $this->asAdmin(); + /** @var Page $page */ + $page = $this->runner->run($import, $parent); + + $pageImages = Image::where('uploaded_to', '=', $page->id)->whereIn('type', ['gallery', 'drawio'])->get(); + $this->assertCount(1, $pageImages); + $this->assertEquals('drawio', $pageImages[0]->type); + + $drawingId = $pageImages[0]->id; + $this->assertStringContainsString("drawio-diagram=\"{$drawingId}\"", $page->html); + $this->assertStringNotContainsString('[[bsexport:image:1125]]', $page->html); + $this->assertStringNotContainsString('drawio-diagram="1125"', $page->html); + + ZipTestHelper::deleteZipForImport($import); + } } From 13a79b3f96c7525d6c4807c5ec83f9d96885f74e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 25 Aug 2025 14:17:55 +0100 Subject: [PATCH 006/139] Shelves: Addressed book edits removing non-visible books Tracks the non-visible existing books on change, to retain as part of the assigned books sync. Added test to cover. For #5728 --- app/Entities/Repos/BookshelfRepo.php | 29 ++++++++++++++++++++++------ tests/Entity/BookShelfTest.php | 29 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php index 8e60f58c4..b870ec377 100644 --- a/app/Entities/Repos/BookshelfRepo.php +++ b/app/Entities/Repos/BookshelfRepo.php @@ -56,20 +56,37 @@ class BookshelfRepo /** * Update which books are assigned to this shelf by syncing the given book ids. - * Function ensures the books are visible to the current user and existing. + * Function ensures the managed books are visible to the current user and existing, + * and that the user does not alter the assignment of books that are not visible to them. */ - protected function updateBooks(Bookshelf $shelf, array $bookIds) + protected function updateBooks(Bookshelf $shelf, array $bookIds): void { $numericIDs = collect($bookIds)->map(function ($id) { return intval($id); }); - $syncData = $this->bookQueries->visibleForList() + $existingBookIds = $shelf->books()->pluck('id')->toArray(); + $visibleExistingBookIds = $this->bookQueries->visibleForList() + ->whereIn('id', $existingBookIds) + ->pluck('id') + ->toArray(); + $nonVisibleExistingBookIds = array_values(array_diff($existingBookIds, $visibleExistingBookIds)); + + $newIdsToAssign = $this->bookQueries->visibleForList() ->whereIn('id', $bookIds) ->pluck('id') - ->mapWithKeys(function ($bookId) use ($numericIDs) { - return [$bookId => ['order' => $numericIDs->search($bookId)]]; - }); + ->toArray(); + + $maxNewIndex = max($numericIDs->keys()->toArray() ?: [0]); + + $syncData = []; + foreach ($newIdsToAssign as $id) { + $syncData[$id] = ['order' => $numericIDs->search($id)]; + } + + foreach ($nonVisibleExistingBookIds as $index => $id) { + $syncData[$id] = ['order' => $maxNewIndex + ($index + 1)]; + } $shelf->books()->sync($syncData); } diff --git a/tests/Entity/BookShelfTest.php b/tests/Entity/BookShelfTest.php index fb9862931..ad1d64e71 100644 --- a/tests/Entity/BookShelfTest.php +++ b/tests/Entity/BookShelfTest.php @@ -259,6 +259,35 @@ class BookShelfTest extends TestCase $this->assertDatabaseHas('bookshelves_books', ['bookshelf_id' => $shelf->id, 'book_id' => $booksToInclude[1]->id]); } + public function test_shelf_edit_does_not_alter_books_we_dont_have_access_to() + { + $shelf = $this->entities->shelf(); + $shelf->books()->detach(); + $this->entities->book(); + $this->entities->book(); + + $newBooks = [$this->entities->book(), $this->entities->book()]; + $originalBooks = [$this->entities->book(), $this->entities->book()]; + foreach ($originalBooks as $book) { + $this->permissions->disableEntityInheritedPermissions($book); + $shelf->books()->attach($book->id); + } + + $this->asEditor()->put($shelf->getUrl(), [ + 'name' => $shelf->name, + 'books' => "{$newBooks[0]->id},{$newBooks[1]->id}", + ])->assertRedirect($shelf->getUrl()); + + $resultingBooksById = $shelf->books()->get()->keyBy('id')->toArray(); + $this->assertCount(4, $resultingBooksById); + foreach ($newBooks as $book) { + $this->assertArrayHasKey($book->id, $resultingBooksById); + } + foreach ($originalBooks as $book) { + $this->assertArrayHasKey($book->id, $resultingBooksById); + } + } + public function test_shelf_create_new_book() { $shelf = $this->entities->shelf(); From ee994fa2b712ddf51befba2bba0ab106edc5e339 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 25 Aug 2025 15:01:13 +0100 Subject: [PATCH 007/139] Testing: Addressed deprecation in test helper Also updated version in phpunit config --- phpunit.xml | 2 +- tests/Helpers/EntityProvider.php | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 243c3d3bc..a8e725d41 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ diff --git a/tests/Helpers/EntityProvider.php b/tests/Helpers/EntityProvider.php index 22e554f74..c794f9478 100644 --- a/tests/Helpers/EntityProvider.php +++ b/tests/Helpers/EntityProvider.php @@ -6,7 +6,6 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasCoverImage; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\BookshelfRepo; @@ -34,9 +33,9 @@ class EntityProvider ]; /** - * Get an un-fetched page from the system. + * Get an unfetched page from the system. */ - public function page(callable $queryFilter = null): Page + public function page(callable|null $queryFilter = null): Page { /** @var Page $page */ $page = Page::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['page'])->first(); @@ -64,9 +63,9 @@ class EntityProvider } /** - * Get an un-fetched chapter from the system. + * Get an unfetched chapter from the system. */ - public function chapter(callable $queryFilter = null): Chapter + public function chapter(callable|null $queryFilter = null): Chapter { /** @var Chapter $chapter */ $chapter = Chapter::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['chapter'])->first(); @@ -80,9 +79,9 @@ class EntityProvider } /** - * Get an un-fetched book from the system. + * Get an unfetched book from the system. */ - public function book(callable $queryFilter = null): Book + public function book(callable|null $queryFilter = null): Book { /** @var Book $book */ $book = Book::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['book'])->first(); @@ -101,9 +100,9 @@ class EntityProvider } /** - * Get an un-fetched shelf from the system. + * Get an unfetched shelf from the system. */ - public function shelf(callable $queryFilter = null): Bookshelf + public function shelf(callable|null $queryFilter = null): Bookshelf { /** @var Bookshelf $shelf */ $shelf = Bookshelf::query()->when($queryFilter, $queryFilter)->whereNotIn('id', $this->fetchCache['bookshelf'])->first(); From 849bc4d6c3b1fbeb09035a0099509a00c2684b93 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 26 Aug 2025 14:41:42 +0100 Subject: [PATCH 008/139] Lexical: Improved nested details interaction - Set to open by default on insert. - Updated selection handling not to always fully cascade to lowest editable child on selection, so parents can be reliably selected. - Updated mouse handling to treat details panes like the root element, inserting within-details where relevant. --- resources/js/wysiwyg/index.ts | 5 ++- .../js/wysiwyg/lexical/core/LexicalNode.ts | 8 ++++ .../wysiwyg/lexical/core/LexicalSelection.ts | 6 +-- .../lexical/core/nodes/LexicalElementNode.ts | 32 ++++++++++++++- .../lexical/rich-text/LexicalDetailsNode.ts | 15 ++++++- .../js/wysiwyg/services/mouse-handling.ts | 41 +++++++++++-------- .../js/wysiwyg/ui/defaults/buttons/objects.ts | 2 + resources/sass/_editor.scss | 10 +++++ 8 files changed, 96 insertions(+), 23 deletions(-) diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index e01b4e8f4..9928ad8bf 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -1,4 +1,4 @@ -import {createEditor, LexicalEditor} from 'lexical'; +import {createEditor} from 'lexical'; import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import {registerRichText} from '@lexical/rich-text'; import {mergeRegister} from '@lexical/utils'; @@ -89,6 +89,9 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st window.debugEditorState = () => { return editor.getEditorState().toJSON(); }; + context.manager.onSelectionChange((selection) => { + console.log(selection, context.editor.getEditorState()); + }); registerCommonNodeMutationListeners(context); diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts index 6d79c01cc..95a723469 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts @@ -383,6 +383,14 @@ export class LexicalNode { return isSelected; } + /** + * Indicate if this node should be selected directly instead of the default + * where the selection would descend to the nearest initial child element. + */ + shouldSelectDirectly(): boolean { + return false; + } + /** * Returns this nodes key. */ diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index 297286a4b..7051336d5 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -476,12 +476,12 @@ export class RangeSelection implements BaseSelection { const startOffset = firstPoint.offset; const endOffset = lastPoint.offset; - if ($isElementNode(firstNode)) { + if ($isElementNode(firstNode) && !firstNode.shouldSelectDirectly()) { const firstNodeDescendant = firstNode.getDescendantByIndex(startOffset); firstNode = firstNodeDescendant != null ? firstNodeDescendant : firstNode; } - if ($isElementNode(lastNode)) { + if ($isElementNode(lastNode) && !lastNode.shouldSelectDirectly()) { let lastNodeDescendant = lastNode.getDescendantByIndex(endOffset); // We don't want to over-select, as node selection infers the child before @@ -499,7 +499,7 @@ export class RangeSelection implements BaseSelection { let nodes: Array; if (firstNode.is(lastNode)) { - if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0) { + if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0 && !firstNode.shouldSelectDirectly()) { nodes = []; } else { nodes = [firstNode]; diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts index a27603773..ce4dbc467 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts @@ -150,6 +150,20 @@ export class ElementNode extends LexicalNode { } return node; } + getFirstSelectableDescendant(): null | T { + if (this.shouldSelectDirectly()) { + return null; + } + let node = this.getFirstChild(); + while ($isElementNode(node) && !node.shouldSelectDirectly()) { + const child = node.getFirstChild(); + if (child === null) { + break; + } + node = child; + } + return node; + } getLastDescendant(): null | T { let node = this.getLastChild(); while ($isElementNode(node)) { @@ -161,6 +175,20 @@ export class ElementNode extends LexicalNode { } return node; } + getLastSelectableDescendant(): null | T { + if (this.shouldSelectDirectly()) { + return null; + } + let node = this.getLastChild(); + while ($isElementNode(node) && !node.shouldSelectDirectly()) { + const child = node.getLastChild(); + if (child === null) { + break; + } + node = child; + } + return node; + } getDescendantByIndex(index: number): null | T { const children = this.getChildren(); const childrenLength = children.length; @@ -319,11 +347,11 @@ export class ElementNode extends LexicalNode { return selection; } selectStart(): RangeSelection { - const firstNode = this.getFirstDescendant(); + const firstNode = this.getFirstSelectableDescendant(); return firstNode ? firstNode.selectStart() : this.select(); } selectEnd(): RangeSelection { - const lastNode = this.getLastDescendant(); + const lastNode = this.getLastSelectableDescendant(); return lastNode ? lastNode.selectEnd() : this.select(); } clear(): this { diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts index 3c845359a..5e5d6b735 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts @@ -75,6 +75,9 @@ export class DetailsNode extends ElementNode { if (this.__open) { el.setAttribute('open', 'true'); + el.removeAttribute('contenteditable'); + } else { + el.setAttribute('contenteditable', 'false'); } const summary = document.createElement('summary'); @@ -84,7 +87,7 @@ export class DetailsNode extends ElementNode { event.preventDefault(); _editor.update(() => { this.select(); - }) + }); }); el.append(summary); @@ -96,6 +99,11 @@ export class DetailsNode extends ElementNode { if (prevNode.__open !== this.__open) { dom.toggleAttribute('open', this.__open); + if (this.__open) { + dom.removeAttribute('contenteditable'); + } else { + dom.setAttribute('contenteditable', 'false'); + } } return prevNode.__id !== this.__id @@ -144,6 +152,7 @@ export class DetailsNode extends ElementNode { } element.removeAttribute('open'); + element.removeAttribute('contenteditable'); return {element}; } @@ -165,6 +174,10 @@ export class DetailsNode extends ElementNode { return node; } + shouldSelectDirectly(): boolean { + return true; + } + } export function $createDetailsNode() { diff --git a/resources/js/wysiwyg/services/mouse-handling.ts b/resources/js/wysiwyg/services/mouse-handling.ts index 058efc8d2..100dd44ff 100644 --- a/resources/js/wysiwyg/services/mouse-handling.ts +++ b/resources/js/wysiwyg/services/mouse-handling.ts @@ -1,31 +1,41 @@ import {EditorUiContext} from "../ui/framework/core"; import { - $createParagraphNode, $getRoot, - $getSelection, + $createParagraphNode, $getNearestNodeFromDOMNode, $getRoot, $isDecoratorNode, CLICK_COMMAND, - COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, - KEY_BACKSPACE_COMMAND, - KEY_DELETE_COMMAND, - KEY_ENTER_COMMAND, KEY_TAB_COMMAND, - LexicalEditor, + COMMAND_PRIORITY_LOW, ElementNode, LexicalNode } from "lexical"; import {$isImageNode} from "@lexical/rich-text/LexicalImageNode"; import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode"; -import {getLastSelection} from "../utils/selection"; -import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes"; -import {$setInsetForSelection} from "../utils/lists"; -import {$isListItemNode} from "@lexical/list"; -import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; import {$isDiagramNode} from "../utils/diagrams"; import {$isTableNode} from "@lexical/table"; +import {$isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; function isHardToEscapeNode(node: LexicalNode): boolean { - return $isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node) || $isTableNode(node); + return $isDecoratorNode(node) + || $isImageNode(node) + || $isMediaNode(node) + || $isDiagramNode(node) + || $isTableNode(node) + || $isDetailsNode(node); +} + +function $getContextNode(event: MouseEvent): ElementNode { + if (event.target instanceof HTMLElement) { + const nearestDetails = event.target.closest('details'); + if (nearestDetails) { + const detailsNode = $getNearestNodeFromDOMNode(nearestDetails); + if ($isDetailsNode(detailsNode)) { + return detailsNode; + } + } + } + return $getRoot(); } function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boolean { - const lastNode = $getRoot().getLastChild(); + const contextNode = $getContextNode(event); + const lastNode = contextNode.getLastChild(); if (!lastNode || !isHardToEscapeNode(lastNode)) { return false; } @@ -40,7 +50,7 @@ function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boole if (isClickBelow) { context.editor.update(() => { const newNode = $createParagraphNode(); - $getRoot().append(newNode); + contextNode.append(newNode); newNode.select(); }); return true; @@ -49,7 +59,6 @@ function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boole return false; } - export function registerMouseHandling(context: EditorUiContext): () => void { const unregisterClick = context.editor.registerCommand(CLICK_COMMAND, (event): boolean => { insertBelowLastNode(context, event); diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts index 41f6061b6..1f78f4487 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts @@ -193,6 +193,8 @@ export const details: EditorButtonDefinition = { .filter(n => n !== null) as ElementNode[]; const uniqueTopLevels = [...new Set(topLevels)]; + detailsNode.setOpen(true); + if (uniqueTopLevels.length > 0) { uniqueTopLevels[0].insertAfter(detailsNode); } else { diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index a7f5ab387..966c11d3a 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -437,6 +437,16 @@ body.editor-is-fullscreen { .editor-node-resizer.active .editor-node-resizer-ghost { display: block; } +.editor-content-area details[contenteditable="false"], +.editor-content-area summary[contenteditable="false"] { + user-select: none; +} +.editor-content-area details[contenteditable="false"] > details * { + pointer-events: none; +} +.editor-content-area details summary { + caret-color: transparent; +} .editor-table-marker { position: fixed; From 519acaf324f5ce806f6e819683756ba175690ea1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 27 Aug 2025 12:51:36 +0100 Subject: [PATCH 009/139] Lexical: Added better selection display for collapisble blocks --- resources/js/wysiwyg/index.ts | 2 + .../js/wysiwyg/services/selection-handling.ts | 49 +++++++++++++++++++ resources/sass/_editor.scss | 4 ++ 3 files changed, 55 insertions(+) create mode 100644 resources/js/wysiwyg/services/selection-handling.ts diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 9928ad8bf..5568b8963 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -20,6 +20,7 @@ import {modals} from "./ui/defaults/modals"; import {CodeBlockDecorator} from "./ui/decorators/code-block"; import {DiagramDecorator} from "./ui/decorators/diagram"; import {registerMouseHandling} from "./services/mouse-handling"; +import {registerSelectionHandling} from "./services/selection-handling"; const theme = { text: { @@ -53,6 +54,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st registerShortcuts(context), registerKeyboardHandling(context), registerMouseHandling(context), + registerSelectionHandling(context), registerTableResizer(editor, context.scrollDOM), registerTableSelectionHandler(editor), registerTaskListHandler(editor, context.editorDOM), diff --git a/resources/js/wysiwyg/services/selection-handling.ts b/resources/js/wysiwyg/services/selection-handling.ts new file mode 100644 index 000000000..e3ea965f2 --- /dev/null +++ b/resources/js/wysiwyg/services/selection-handling.ts @@ -0,0 +1,49 @@ +import {EditorUiContext} from "../ui/framework/core"; +import { + $getSelection, + COMMAND_PRIORITY_LOW, + SELECTION_CHANGE_COMMAND +} from "lexical"; +import {$isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; + + +const trackedDomNodes = new Set(); + +/** + * Set a selection indicator on nodes which require it. + * @param context + */ +function setSelectionIndicator(context: EditorUiContext): boolean { + + for (const domNode of trackedDomNodes) { + domNode.classList.remove('selected'); + trackedDomNodes.delete(domNode); + } + + const selection = $getSelection(); + const nodes = selection?.getNodes() || []; + + if (nodes.length === 1) { + if ($isDetailsNode(nodes[0])) { + const domEl = context.editor.getElementByKey(nodes[0].getKey()); + if (domEl) { + domEl.classList.add('selected'); + trackedDomNodes.add(domEl); + } + } + } + + return false; +} + +export function registerSelectionHandling(context: EditorUiContext): () => void { + const unregisterSelectionChange = context.editor.registerCommand(SELECTION_CHANGE_COMMAND, (): boolean => { + setSelectionIndicator(context); + return false; + }, COMMAND_PRIORITY_LOW); + + + return () => { + unregisterSelectionChange(); + }; +} \ No newline at end of file diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 966c11d3a..cd4bb5d2e 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -447,6 +447,10 @@ body.editor-is-fullscreen { .editor-content-area details summary { caret-color: transparent; } +.editor-content-area details.selected { + outline: 1px dashed var(--editor-color-primary); + outline-offset: 1px; +} .editor-table-marker { position: fixed; From 46613f76f61902db4dca5f54c308b8dadf02a14c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 27 Aug 2025 14:09:38 +0100 Subject: [PATCH 010/139] Lexical: Added backspace handling for details Allows more reliable removal of details block on backspace at first child position with the details block. --- resources/js/wysiwyg/index.ts | 3 -- .../lexical/core/nodes/LexicalElementNode.ts | 2 +- .../lexical/rich-text/LexicalDetailsNode.ts | 4 +++ .../js/wysiwyg/services/keyboard-handling.ts | 34 +++++++++++++++++-- .../js/wysiwyg/ui/defaults/buttons/objects.ts | 7 ++-- resources/js/wysiwyg/utils/details.ts | 9 +++++ 6 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 resources/js/wysiwyg/utils/details.ts diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 5568b8963..2c8c3b952 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -91,9 +91,6 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st window.debugEditorState = () => { return editor.getEditorState().toJSON(); }; - context.manager.onSelectionChange((selection) => { - console.log(selection, context.editor.getEditorState()); - }); registerCommonNodeMutationListeners(context); diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts index ce4dbc467..0329aed6f 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts @@ -307,7 +307,7 @@ export class ElementNode extends LexicalNode { let anchorOffset = _anchorOffset; let focusOffset = _focusOffset; const childrenCount = this.getChildrenSize(); - if (!this.canBeEmpty()) { + if (!this.canBeEmpty() && !this.shouldSelectDirectly()) { if (_anchorOffset === 0 && _focusOffset === 0) { const firstChild = this.getFirstChild(); if ($isTextNode(firstChild) || $isElementNode(firstChild)) { diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts index 5e5d6b735..cdf32fdcb 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts @@ -178,6 +178,10 @@ export class DetailsNode extends ElementNode { return true; } + canBeEmpty(): boolean { + return false; + } + } export function $createDetailsNode() { diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index b4f546117..aba0b1aa8 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -18,6 +18,7 @@ import {$setInsetForSelection} from "../utils/lists"; import {$isListItemNode} from "@lexical/list"; import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; import {$isDiagramNode} from "../utils/diagrams"; +import {$unwrapDetailsNode} from "../utils/details"; function isSingleSelectedNode(nodes: LexicalNode[]): boolean { if (nodes.length === 1) { @@ -172,6 +173,35 @@ function getDetailsScenario(editor: LexicalEditor): { } } +function unwrapDetailsNode(context: EditorUiContext, event: KeyboardEvent): boolean { + const selection = $getSelection(); + const nodes = selection?.getNodes() || []; + + if (nodes.length !== 1) { + return false; + } + + const selectedNearestBlock = $getNearestNodeBlockParent(nodes[0]); + if (!selectedNearestBlock) { + return false; + } + + const selectedParentBlock = selectedNearestBlock.getParent(); + const selectRange = selection?.getStartEndPoints(); + + if (selectRange && $isDetailsNode(selectedParentBlock) && selectRange[0].offset === 0 && selectedNearestBlock.getIndexWithinParent() === 0) { + event.preventDefault(); + context.editor.update(() => { + $unwrapDetailsNode(selectedParentBlock); + selectedNearestBlock.selectStart(); + context.manager.triggerLayoutUpdate(); + }); + return true; + } + + return false; +} + function $isSingleListItem(nodes: LexicalNode[]): boolean { if (nodes.length !== 1) { return false; @@ -201,9 +231,9 @@ function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null): boo } export function registerKeyboardHandling(context: EditorUiContext): () => void { - const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => { + const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (event): boolean => { deleteSingleSelectedNode(context.editor); - return false; + return unwrapDetailsNode(context, event); }, COMMAND_PRIORITY_LOW); const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => { diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts index 1f78f4487..00dc9500e 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts @@ -34,6 +34,7 @@ import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images"; import {$showDetailsForm, $showImageForm, $showLinkForm, $showMediaForm} from "../forms/objects"; import {formatCodeBlock} from "../../../utils/formats"; +import {$unwrapDetailsNode} from "../../../utils/details"; export const link: EditorButtonDefinition = { label: 'Insert/edit link', @@ -251,11 +252,7 @@ export const detailsUnwrap: EditorButtonDefinition = { context.editor.update(() => { const details = $getNodeFromSelection($getSelection(), $isDetailsNode); if ($isDetailsNode(details)) { - const children = details.getChildren(); - for (const child of children) { - details.insertBefore(child); - } - details.remove(); + $unwrapDetailsNode(details); context.manager.triggerLayoutUpdate(); } }) diff --git a/resources/js/wysiwyg/utils/details.ts b/resources/js/wysiwyg/utils/details.ts new file mode 100644 index 000000000..79e8600d0 --- /dev/null +++ b/resources/js/wysiwyg/utils/details.ts @@ -0,0 +1,9 @@ +import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; + +export function $unwrapDetailsNode(node: DetailsNode) { + const children = node.getChildren(); + for (const child of children) { + node.insertBefore(child); + } + node.remove(); +} \ No newline at end of file From f5da31037d79a5701610ef8f619ab3c2b38b5fa6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 Aug 2025 11:17:18 +0100 Subject: [PATCH 011/139] Lexical: Fixed details tests Updated to use new test pattern while there. --- .../__tests__/unit/LexicalDetailsNode.test.ts | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDetailsNode.test.ts b/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDetailsNode.test.ts index faa31d887..828aad839 100644 --- a/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDetailsNode.test.ts +++ b/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDetailsNode.test.ts @@ -1,6 +1,5 @@ -import {dispatchKeydownEventForNode, initializeUnitTest} from "lexical/__tests__/utils"; -import {$createDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; -import {$createParagraphNode, $getRoot, LexicalNode, ParagraphNode} from "lexical"; +import {createTestContext} from "lexical/__tests__/utils"; +import {$createDetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; const editorConfig = Object.freeze({ namespace: '', @@ -9,32 +8,28 @@ const editorConfig = Object.freeze({ }); describe('LexicalDetailsNode tests', () => { - initializeUnitTest((testEnv) => { + test('createDOM()', () => { + const {editor} = createTestContext(); + let html!: string; - test('createDOM()', () => { - const {editor} = testEnv; - let html!: string; - - editor.updateAndCommit(() => { - const details = $createDetailsNode(); - html = details.createDOM(editorConfig, editor).outerHTML; - }); - - expect(html).toBe(`
`); + editor.updateAndCommit(() => { + const details = $createDetailsNode(); + html = details.createDOM(editorConfig, editor).outerHTML; }); - test('exportDOM()', () => { - const {editor} = testEnv; - let html!: string; + expect(html).toBe(`
`); + }); - editor.updateAndCommit(() => { - const details = $createDetailsNode(); - html = (details.exportDOM(editor).element as HTMLElement).outerHTML; - }); + test('exportDOM()', () => { + const {editor} = createTestContext(); + let html!: string; - expect(html).toBe(`
`); + editor.updateAndCommit(() => { + const details = $createDetailsNode(); + details.setSummary('Hello there<>!') + html = (details.exportDOM(editor).element as HTMLElement).outerHTML; }); - + expect(html).toBe(`
Hello there<>!
`); }); }) \ No newline at end of file From 0436ccfebf7a22459f9272206fd8ef35cdf3b396 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 Aug 2025 14:59:36 +0100 Subject: [PATCH 012/139] Updated translations with latest Crowdin changes (#5759) --- lang/ca/activities.php | 42 +++++++++--------- lang/ca/common.php | 6 +-- lang/ca/editor.php | 10 ++--- lang/ca/entities.php | 90 +++++++++++++++++++-------------------- lang/ca/errors.php | 38 ++++++++--------- lang/ca/notifications.php | 2 +- lang/ca/settings.php | 76 ++++++++++++++++----------------- lang/ca/validation.php | 8 ++-- lang/da/auth.php | 22 +++++----- lang/da/common.php | 8 ++-- lang/da/components.php | 4 +- lang/da/entities.php | 10 ++--- lang/da/passwords.php | 2 +- lang/da/preferences.php | 8 ++-- lang/da/settings.php | 4 +- lang/et/entities.php | 8 ++-- lang/ja/entities.php | 8 ++-- lang/nl/activities.php | 2 +- lang/nl/common.php | 2 +- lang/nl/editor.php | 2 +- lang/nl/entities.php | 50 +++++++++++----------- lang/nl/errors.php | 10 ++--- lang/nl/notifications.php | 4 +- lang/nl/preferences.php | 2 +- lang/nl/settings.php | 6 +-- lang/ru/editor.php | 8 ++-- lang/uk/common.php | 4 +- lang/uk/editor.php | 2 +- lang/uk/entities.php | 32 +++++++------- lang/vi/editor.php | 2 +- lang/vi/entities.php | 12 +++--- 31 files changed, 242 insertions(+), 242 deletions(-) diff --git a/lang/ca/activities.php b/lang/ca/activities.php index 8decaa739..4894c279a 100644 --- a/lang/ca/activities.php +++ b/lang/ca/activities.php @@ -18,35 +18,35 @@ return [ 'page_move_notification' => 'S’ha mogut la pàgina', // Chapters - 'chapter_create' => 'ha creat el capítol', + 'chapter_create' => 'S\'ha creat el capítol', 'chapter_create_notification' => 'S’ha creat el capítol', 'chapter_update' => 'ha actualitzat el capítol', 'chapter_update_notification' => 'S’ha actualitzat el capítol', 'chapter_delete' => 'ha suprimit el capítol', 'chapter_delete_notification' => 'S’ha suprimit el capítol', - 'chapter_move' => 'ha mogut el capítol', + 'chapter_move' => 's\'ha mogut el capítol', 'chapter_move_notification' => 'S’ha mogut el capítol', // Books - 'book_create' => 'ha creat el llibre', + 'book_create' => 'llibre creat', 'book_create_notification' => 'S’ha creat el llibre', 'book_create_from_chapter' => 'ha convertit el capítol a llibre', 'book_create_from_chapter_notification' => 'S’ha convertit el capítol a llibre', - 'book_update' => 'ha actualitzat el llibre', + 'book_update' => 'llibre actualitzat', 'book_update_notification' => 'S’ha actualitzat el llibre', - 'book_delete' => 'ha suprimit el llibre', + 'book_delete' => 'llibre suprimit', 'book_delete_notification' => 'S’ha suprimit el llibre', - 'book_sort' => 'ha ordenat el llibre', + 'book_sort' => 'llibre ordenat', 'book_sort_notification' => 'S’ha tornat a ordenar el llibre', // Bookshelves 'bookshelf_create' => 'ha creat el prestatge', 'bookshelf_create_notification' => 'S’ha creat el prestatge', - 'bookshelf_create_from_book' => 'ha convertit el llibre a prestatge', + 'bookshelf_create_from_book' => 'llibre convertit a prestatge', 'bookshelf_create_from_book_notification' => 'S’ha convertit el llibre a prestatge', - 'bookshelf_update' => 'ha actualitzat el prestatge', + 'bookshelf_update' => 'prestatge actualitzat', 'bookshelf_update_notification' => 'S’ha actualitzat el prestatge', - 'bookshelf_delete' => 'ha suprimit el prestatge', + 'bookshelf_delete' => 'prestatge suprimit', 'bookshelf_delete_notification' => 'S’ha suprimit el prestatge', // Revisions @@ -85,12 +85,12 @@ return [ 'webhook_delete_notification' => 'S’ha suprimit el 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' => 'importació creada', + 'import_create_notification' => 'L\'importació s\'ha carregat correctament', + 'import_run' => 'importació actualitzada', + 'import_run_notification' => 'Contingut importat correctament', + 'import_delete' => 'importació eliminada', + 'import_delete_notification' => 'Importació eliminada correctament', // Users 'user_create' => 'ha creat l’usuari', @@ -128,12 +128,12 @@ return [ 'comment_delete' => 'ha suprimit un comentari', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'crear regla d\'ordenació', + 'sort_rule_create_notification' => 'Regla d\'ordenació creada correctament', + 'sort_rule_update' => 'regla d\'ordenació actualitzada', + 'sort_rule_update_notification' => 'Regla d\'ordenació actualitzada correctament', + 'sort_rule_delete' => 'regla d\'ordenació eliminada', + 'sort_rule_delete_notification' => 'Regla d\'ordenació eliminada correctament', // Other 'permissions_update' => 'ha actualitzat els permisos', diff --git a/lang/ca/common.php b/lang/ca/common.php index 698956601..8976edaa1 100644 --- a/lang/ca/common.php +++ b/lang/ca/common.php @@ -30,8 +30,8 @@ return [ 'create' => 'Crea', 'update' => 'Actualitza', 'edit' => 'Edita', - 'archive' => 'Archive', - 'unarchive' => 'Un-Archive', + 'archive' => 'Arxivar', + 'unarchive' => 'Desarxivar', 'sort' => 'Ordena', 'move' => 'Mou', 'copy' => 'Copia', @@ -111,5 +111,5 @@ return [ 'terms_of_service' => 'Condicions del servei', // OpenSearch - 'opensearch_description' => 'Search :appName', + 'opensearch_description' => 'Buscar :appName', ]; diff --git a/lang/ca/editor.php b/lang/ca/editor.php index 8cc04c1ca..e19d82f8f 100644 --- a/lang/ca/editor.php +++ b/lang/ca/editor.php @@ -25,7 +25,7 @@ return [ 'width' => 'Amplada', 'height' => 'Altura', 'More' => 'Més', - 'select' => 'Selecciona…', + 'select' => 'Selecciona …', // Toolbar 'formats' => 'Formats', @@ -48,7 +48,7 @@ return [ 'superscript' => 'Superíndex', 'subscript' => 'Subíndex', 'text_color' => 'Color del text', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Color ressaltat', 'custom_color' => 'Color personalitzat', 'remove_color' => 'Elimina el color', 'background_color' => 'Color de fons', @@ -149,7 +149,7 @@ return [ 'url' => 'URL', 'text_to_display' => 'Text per a mostrar', 'title' => 'Títol', - 'browse_links' => 'Browse links', + 'browse_links' => 'Explorar enllaços', 'open_link' => 'Obre l’enllaç', 'open_link_in' => 'Obre l’enllaç…', 'open_link_current' => 'A la finestra actual', @@ -166,8 +166,8 @@ return [ 'about' => 'Quant a l’Editor', 'about_title' => 'Quant a l’Editor WYSIWYG', 'editor_license' => 'Llicència i copyright de l’Editor', - 'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.', - 'editor_lexical_license_link' => 'Full license details can be found here.', + 'editor_lexical_license' => 'Aquest editor està construït com una bifurcació de :lexicalLink i es distribueix sota la llicència MIT.', + 'editor_lexical_license_link' => 'Tots els detalls complets de la llicència es poden trobar aquí.', 'editor_tiny_license' => 'Aquest editor s’ha creat amb :tinyLink que es proporciona amb la llicència MIT.', 'editor_tiny_license_link' => 'Detalls de la llicència i el copyright de TinyMCE.', 'save_continue' => 'Desa la pàgina i continua', diff --git a/lang/ca/entities.php b/lang/ca/entities.php index 51bb4aecb..108a38c2c 100644 --- a/lang/ca/entities.php +++ b/lang/ca/entities.php @@ -39,34 +39,34 @@ return [ 'export_pdf' => 'Fitxer PDF', 'export_text' => 'Fitxer de text sense format', 'export_md' => 'Fitxer Markdown', - '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:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'export_zip' => 'ZIP portable', + 'default_template' => 'Plantilla de pàgina per defecte', + 'default_template_explain' => 'Assigna una plantilla de pàgina que s\'utilitzarà com a contingut predeterminat per a totes les pàgines creades en aquest element. Tingues en compte que això només s\'utilitzarà si el creador de pàgines té accés a la plantilla de pàgina triada.', + 'default_template_select' => 'Seleccioneu una plantilla de pàgina', + 'import' => 'Importar', + 'import_validate' => 'Validar importació', + 'import_desc' => 'Importar llibres, capítols i pàgines utilitzant una exportació ZIP portable de la mateixa o una altra instància. Selecciona un arxiu ZIP per continuar. Després que l\'arxiu s\'hagi penjat i validat, podrà configurar i confirmar l\'importació en la següent vista.', + 'import_zip_select' => 'Seleccioneu un fitxer ZIP per pujar', + 'import_zip_validation_errors' => 'S\'han detectat errors al validar l\'arxiu ZIP proporcionat:', + 'import_pending' => 'Importació pendent', + 'import_pending_none' => 'No s\'han iniciat les importacions.', + 'import_continue' => 'Continuar importació', + 'import_continue_desc' => 'Revisa el contingut que s\'ha d\'importar de l\'arxiu ZIP penjat. Quan estigui llest, executa l\'importació per afegir el seu contingut al sistema. L\'arxiu d\'importació ZIP penjat s\'eliminarà automàticament al finalitzar l\'importació correctament.', + 'import_details' => 'Detalls d\'importació', + 'import_run' => 'Executar importació', + 'import_size' => ':size Mida de l\'arxiu ZIP', + 'import_uploaded_at' => 'Penjat :relativeTime', + 'import_uploaded_by' => 'Actualitzat per', + 'import_location' => 'Importar ubicació', + 'import_location_desc' => 'Selecciona una ubicació de destí pel contingut importat. Necessitarà els permisos pertinents per crear-lo dins de la ubicació triada.', + 'import_delete_confirm' => 'Esteu segur que voleu suplir aquesta importació?', + 'import_delete_desc' => 'Això eliminarà l\'arxiu ZIP d\'importació penjat i no es pot desfer.', + 'import_errors' => 'Importar errors', + 'import_errors_desc' => 'S\'han produït els següents errors durant l\'intent d\'importació:', + 'breadcrumb_siblings_for_page' => 'Navegar entre pàgines del mateix nivell', + 'breadcrumb_siblings_for_chapter' => 'Navegar entre capítols del mateix nivell', + 'breadcrumb_siblings_for_book' => 'Navegar entre llibres del mateix nivell', + 'breadcrumb_siblings_for_bookshelf' => 'Navegar entre llibreries del mateix nivell', // Permissions and restrictions 'permissions' => 'Permisos', @@ -170,9 +170,9 @@ return [ 'books_search_this' => 'Cerca en aquest llibre', 'books_navigation' => 'Navegació del llibre', 'books_sort' => 'Ordena el contingut d’un llibre', - '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_desc' => 'Mou capítols i pàgines dins d\'un llibre per reorganitzar el seu contingut. Es poden afegir altres llibres que permetin moure fàcilment capítols i pàgines entre llibres. De manera opcional, es poden establir regles d\'ordenació automàtica per ordenar automàticament el contingut d\'aquest llibre quan hi hagi canvis.', + 'books_sort_auto_sort' => 'Opció d\'ordenació automàtica', + 'books_sort_auto_sort_active' => 'Opció d\'ordenació activa :sortName', 'books_sort_named' => 'Ordena el llibre «:bookName»', 'books_sort_name' => 'Ordena pel nom', 'books_sort_created' => 'Ordena per la data de creació', @@ -234,7 +234,7 @@ return [ 'pages_delete_draft' => 'Suprimeix l’esborrany de pàgina', 'pages_delete_success' => 'S’ha suprimit la pàgina', 'pages_delete_draft_success' => 'S’ha suprimit l’esborrany de pàgina', - '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' => 'Aquesta pàgina està en ús com a plantilla de pàgina predeterminada de llibre o capítol. Aquests llibres o capítols ja no tindran una plantilla de pàgina predeterminada assignada després d\'eliminar aquesta pàgina.', 'pages_delete_confirm' => 'Esteu segur que voleu suprimir aquesta pàgina?', 'pages_delete_draft_confirm' => 'Esteu segur que voleu suprimir aquest esborrany de pàgina?', 'pages_editing_named' => 'Edició de la pàgina «:pageName»', @@ -251,8 +251,8 @@ return [ 'pages_edit_switch_to_markdown_clean' => '(Contingut net)', 'pages_edit_switch_to_markdown_stable' => '(Contingut estable)', 'pages_edit_switch_to_wysiwyg' => 'Canvia a l’editor WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', + 'pages_edit_switch_to_new_wysiwyg' => 'Canviar al nou editor WYSIWYG', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(En Beta Test)', 'pages_edit_set_changelog' => 'Registre de canvis', 'pages_edit_enter_changelog_desc' => 'Introduïu una descripció breu dels canvis que heu fet', 'pages_edit_enter_changelog' => 'Registra un canvi', @@ -272,7 +272,7 @@ return [ 'pages_md_insert_drawing' => 'Insereix un dibuix', 'pages_md_show_preview' => 'Mostra la visualització prèvia', 'pages_md_sync_scroll' => 'Sincronitza el desplaçament de la visualització prèvia', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_plain_editor' => 'Editor de text pla', 'pages_drawing_unsaved' => 'S’ha trobat un dibuix sense desar', 'pages_drawing_unsaved_confirm' => 'S’han trobat dades d’un dibuix d’un intent anterior de desar un dibuix. Voleu restaurar aquest dibuix no desat per a reprendre’n l’edició?', 'pages_not_in_chapter' => 'La pàgina no és un capítol', @@ -397,11 +397,11 @@ return [ 'comment' => 'Comentari', 'comments' => 'Comentaris', 'comment_add' => 'Afegeix un comentari', - 'comment_none' => 'No comments to display', + 'comment_none' => 'No hi ha comentaris per mostrar', 'comment_placeholder' => 'Deixa un comentari aquí', - 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', - 'comment_archived_threads' => 'Archived Threads', + 'comment_thread_count' => ':count fil de comentaris|:count fils de comentaris', + 'comment_archived_count' => ':count Arxivats', + 'comment_archived_threads' => 'Fils arxivats', 'comment_save' => 'Desa el comentari', 'comment_new' => 'Crea un comentari', 'comment_created' => 'ha comentat :createDiff', @@ -410,14 +410,14 @@ return [ 'comment_deleted_success' => 'S’ha suprimit el comentari', 'comment_created_success' => 'S’ha afegit un comentari', 'comment_updated_success' => 'S’ha actualitzat un comentari', - 'comment_archive_success' => 'Comment archived', - 'comment_unarchive_success' => 'Comment un-archived', - 'comment_view' => 'View comment', - 'comment_jump_to_thread' => 'Jump to thread', + 'comment_archive_success' => 'Comentari arxivat', + 'comment_unarchive_success' => 'Comentari desarxivat', + 'comment_view' => 'Mostra el comentari', + 'comment_jump_to_thread' => 'Anar al fil', 'comment_delete_confirm' => 'Esteu segur que voleu suprimir aquest comentari?', 'comment_in_reply_to' => 'En resposta a :commentId', - 'comment_reference' => 'Reference', - 'comment_reference_outdated' => '(Outdated)', + 'comment_reference' => 'Referència', + 'comment_reference_outdated' => '(Obsolet)', 'comment_editor_explain' => 'Vet aquí els comentaris que s’han fet en aquesta pàgina. Els comentaris es poden fer i gestionar quan es visualitza la pàgina desada.', // Revision diff --git a/lang/ca/errors.php b/lang/ca/errors.php index 76fb293da..6f3784c2c 100644 --- a/lang/ca/errors.php +++ b/lang/ca/errors.php @@ -6,17 +6,17 @@ return [ // Permissions 'permission' => 'No teniu permís per a accedir a la pàgina sol·licitada.', - 'permissionJson' => 'No teniu permís per a fer l’acció sol·licitada.', + 'permissionJson' => 'No teniu permís per a executar l’acció sol·licitada.', // Auth 'error_user_exists_different_creds' => 'Ja existeix un usuari amb el correu electrònic :email però amb unes credencials diferents.', - 'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details', - 'email_already_confirmed' => 'Ja s’ha confirmat el correu electrònic. Proveu d’iniciar sessió.', + 'auth_pre_register_theme_prevention' => 'El compte d\'usuari no s\'ha pogut registrar amb els detalls proporcionats', + 'email_already_confirmed' => 'L’adreça electrònica ja està confirmada. Proveu d’iniciar la sessió.', 'email_confirmation_invalid' => 'Aquest testimoni de confirmació no és vàlid o ja s’ha utilitzat. Proveu de tornar-vos a registrar.', 'email_confirmation_expired' => 'Aquest testimoni de confirmació ha caducat. S’ha enviat un altre correu electrònic de confirmació.', - 'email_confirmation_awaiting' => 'Cal confirmar l’adreça electrònica del compte que utilitzeu.', + 'email_confirmation_awaiting' => 'Cal confirmar l’adreça electrònica del compte que utilitzeu', 'ldap_fail_anonymous' => 'L’accés LDAP anònim ha fallat', - 'ldap_fail_authed' => 'L’accés LDAP amb el nom distintiu i la contrasenya proporcionades', + 'ldap_fail_authed' => 'L’accés LDAP amb el nom distintiu i la contrasenya proporcionats ha fallat', 'ldap_extension_not_installed' => 'L’extensió PHP de l’LDAP no està instal·lada', 'ldap_cannot_connect' => 'No s’ha pogut connectar amb el servidor LDAP perquè la connexió inicial ha fallat', 'saml_already_logged_in' => 'Ja heu iniciat sessió', @@ -29,15 +29,15 @@ return [ 'social_no_action_defined' => 'No s’ha definit cap acció', 'social_login_bad_response' => "S’ha produït un error en l’inici de sessió amb :socialAccount: \n:error", 'social_account_in_use' => 'Aquest compte de :socialAccount ja s’està utilitzant. Proveu d’iniciar sessió amb :socialAccount.', - 'social_account_email_in_use' => 'L’adreça electrònica :email ja s’està utilitzant. Si ja teniu uns compte podeu connectar-hi el vostre compte de :socialAccount des de la configuració del vostre perfil.', + 'social_account_email_in_use' => 'L’adreça electrònica :email ja està en ús. Si ja teniu un compte, podeu connectar-hi el vostre compte de :socialAccount a la configuració del vostre perfil.', 'social_account_existing' => 'Aquest compte de :socialAccount ja està associat al vostre perfil.', 'social_account_already_used_existing' => 'Aquest compte de :socialAccount ja està associat a un altre usuari.', 'social_account_not_used' => 'Aquest compte de :socialAccount no està associat a cap usuari. Associeu-lo a la configuració del vostre perfil. ', 'social_account_register_instructions' => 'Si encara no teniu un compte, podeu registrar-vos amb l’opció :socialAccount.', 'social_driver_not_found' => 'No s’ha trobat el controlador social', 'social_driver_not_configured' => 'La configuració de :socialAccount no és correcta.', - 'invite_token_expired' => 'Aquest enllaç d’invitació ha caducat. Proveu de reinicialitzar la contrasenya.', - 'login_user_not_found' => 'A user for this action could not be found.', + 'invite_token_expired' => 'Aquest enllaç d’invitació ha caducat. Podeu provar de restablir la contrasenya del vostre compte.', + 'login_user_not_found' => 'No s\'ha pogut trobar un usuari per aquesta acció.', // System 'path_not_writable' => 'No s’ha pogut pujar a :filePath. Assegureu-vos que teniu permisos d’escriptura al servidor.', @@ -78,7 +78,7 @@ return [ // Users 'users_cannot_delete_only_admin' => 'No podeu suprimir l’administrador únic.', 'users_cannot_delete_guest' => 'No podeu suprimir l’usuari convidat.', - 'users_could_not_send_invite' => 'Could not create user since invite email failed to send', + 'users_could_not_send_invite' => 'No s\'ha pogut crear l\'usuari, ja que no s\'ha pogut enviar el correu d\'invitació', // Roles 'role_cannot_be_edited' => 'No es pot editar aquest rol.', @@ -106,16 +106,16 @@ return [ 'back_soon' => 'Aviat ho arreglarem.', // Import - 'import_zip_cant_read' => 'Could not read ZIP file.', - 'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.', - 'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.', - 'import_validation_failed' => 'Import ZIP failed to validate with errors:', - 'import_zip_failed_notification' => 'Failed to import ZIP file.', - 'import_perms_books' => 'You are lacking the required permissions to create books.', - 'import_perms_chapters' => 'You are lacking the required permissions to create chapters.', - 'import_perms_pages' => 'You are lacking the required permissions to create pages.', - 'import_perms_images' => 'You are lacking the required permissions to create images.', - 'import_perms_attachments' => 'You are lacking the required permission to create attachments.', + 'import_zip_cant_read' => 'No es pot llegir el fitxer ZIP.', + 'import_zip_cant_decode_data' => 'No s\'ha pogut trobar i descodificar el fitxer data.json en el fitxer ZIP.', + 'import_zip_no_data' => 'Les dades del fitxer ZIP no contenen cap llibre, capítol o contingut de pàgina.', + 'import_validation_failed' => 'Error en validar la importació del ZIP amb els errors:', + 'import_zip_failed_notification' => 'Error en importar l\'arxiu ZIP.', + 'import_perms_books' => 'Li falten els permisos necessaris per crear llibres.', + 'import_perms_chapters' => 'Li falten els permisos necessaris per crear capítols.', + 'import_perms_pages' => 'Li falten els permisos necessaris per crear pàgines.', + 'import_perms_images' => 'Li falten els permisos necessaris per crear imatges.', + 'import_perms_attachments' => 'Li falten els permisos necessaris per crear adjunts.', // API errors 'api_no_authorization_found' => 'No s’ha trobat cap testimoni d’autorització en aquesta sol·licitud.', diff --git a/lang/ca/notifications.php b/lang/ca/notifications.php index fdea33436..49b986ad0 100644 --- a/lang/ca/notifications.php +++ b/lang/ca/notifications.php @@ -13,7 +13,7 @@ return [ 'updated_page_debounce' => 'Per a evitar que s’acumulin les notificacions, durant un temps no se us notificarà cap canvi fet en aquesta pàgina pel mateix usuari.', 'detail_page_name' => 'Nom de la pàgina:', - 'detail_page_path' => 'Camí de la pàgina:', + 'detail_page_path' => 'Ruta de la pàgina:', 'detail_commenter' => 'Autor del comentari:', 'detail_comment' => 'Comentari:', 'detail_created_by' => 'Creada per:', diff --git a/lang/ca/settings.php b/lang/ca/settings.php index e25a4a032..63352147e 100644 --- a/lang/ca/settings.php +++ b/lang/ca/settings.php @@ -8,7 +8,7 @@ return [ // Common Messages 'settings' => 'Configuració', - 'settings_save' => 'Configuració de desat', + 'settings_save' => 'Guardar configuració', 'system_version' => 'Versió de sistema', 'categories' => 'Categories', @@ -19,8 +19,8 @@ return [ 'app_name_desc' => 'El nom es mostra a la capçalera i als correus electrònics enviats pel sistema.', 'app_name_header' => 'Mostra el nom a la capçalera', 'app_public_access' => 'Accés públic', - 'app_public_access_desc' => 'Si activeu aquesta opció les visitants podran accedir a la vostra instància del BookStack sense iniciar sessió.', - 'app_public_access_desc_guest' => 'L’accés per als visitants públics es pot gestionar amb l’usuari «Convidat».', + 'app_public_access_desc' => 'Si activeu aquesta opció permetrà als visitants, accedir a la vostra instància del BookStack sense iniciar sessió.', + 'app_public_access_desc_guest' => 'L’accés per als visitants públics es pot gestionar amb l’usuari Convidat.', 'app_public_access_toggle' => 'Permet l’accés públic', 'app_public_viewing' => 'Esteu segur que voleu permetre l’accés públic?', 'app_secure_images' => 'Pujada d’imatges amb més seguretat', @@ -75,34 +75,34 @@ return [ 'reg_confirm_restrict_domain_placeholder' => 'No hi ha cap restricció', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', - 'sorting_rules' => 'Sort Rules', - 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', - 'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books', - 'sort_rule_create' => 'Create Sort Rule', - 'sort_rule_edit' => 'Edit Sort Rule', - 'sort_rule_delete' => 'Delete Sort Rule', - 'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.', - 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?', - 'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?', - 'sort_rule_details' => 'Sort Rule Details', - 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.', - 'sort_rule_operations' => 'Sort Operations', - 'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.', - 'sort_rule_available_operations' => 'Available Operations', - 'sort_rule_available_operations_empty' => 'No operations remaining', - 'sort_rule_configured_operations' => 'Configured Operations', - 'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list', + 'sorting' => 'Ordenar', + 'sorting_book_default' => 'Ordre predeterminat del llibre', + 'sorting_book_default_desc' => 'Selecciona la regla d\'ordenació predeterminada per aplicar a nous llibres. Això no afectarà els llibres existents, i pot ser anul·lat per llibre.', + 'sorting_rules' => 'Regles d\'ordenació', + 'sorting_rules_desc' => 'Són operacions d\'ordenació predefinides que es poden aplicar al contingut en el sistema.', + 'sort_rule_assigned_to_x_books' => 'Assignat a :count llibre | Assignat a :count llibres', + 'sort_rule_create' => 'Crear regla d\'ordenació', + 'sort_rule_edit' => 'Editar regla d\'ordenació', + 'sort_rule_delete' => 'Eliminar regla d\'ordenació', + 'sort_rule_delete_desc' => 'Eliminar aquesta regla d\'ordenació del sistema. Els llibres que utilitzin aquest tipus, es revertiran a l\'ordenació manual.', + 'sort_rule_delete_warn_books' => 'Aquesta regla d\'ordenació s\'utilitza actualment en :count llibre(s). Està segur que vol eliminar-la?', + 'sort_rule_delete_warn_default' => 'Aquesta regla d\'ordenació s\'utilitza actualment com a predeterminada per als llibres. Està segur que vol eliminar-la?', + 'sort_rule_details' => 'Detalls de la regla d\'ordenació', + 'sort_rule_details_desc' => 'Defineix un nom per aquesta regla d\'ordenació, que apareixerà a les llistes quan els usuaris estiguin seleccionant un ordre.', + 'sort_rule_operations' => 'Operacions d\'ordenació', + 'sort_rule_operations_desc' => 'Configura les accions d\'ordenació a realitzar movent-les de la llista d\'operacions disponibles. En utilitzar-se, les operacions s\'aplicaran en ordre, de dalt cap a baix. Qualsevol canvi realitzat aquí, s\'aplicarà a tots els llibres assignats en guardar.', + 'sort_rule_available_operations' => 'Operacions disponibles', + 'sort_rule_available_operations_empty' => 'No hi ha operacions pendents', + 'sort_rule_configured_operations' => 'Operacions configurades', + 'sort_rule_configured_operations_empty' => 'Arrossegar/afegir operacions des de la llista d\'Operacions Disponibles', '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', + 'sort_rule_op_name' => 'Nom - Alfabètic', + 'sort_rule_op_name_numeric' => 'Nom - Numèric', + 'sort_rule_op_created_date' => 'Data de creació', + 'sort_rule_op_updated_date' => 'Data d\'actualització', + 'sort_rule_op_chapters_first' => 'Capítols a l\'inici', + 'sort_rule_op_chapters_last' => 'Capítols al final', // Maintenance settings 'maint' => 'Manteniment', @@ -139,7 +139,7 @@ return [ 'recycle_bin_contents_empty' => 'La paperera és buida', 'recycle_bin_empty' => 'Buida la paperera', 'recycle_bin_empty_confirm' => 'Se suprimiran permanentment tots els elements que hi ha a la paperera incloent-hi el contingut que hi hagi a cada element. Esteu segur que voleu buidar la paperera?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?', + 'recycle_bin_destroy_confirm' => 'Aquesta acció suprimirà del sistema de manera permanent aquest element, juntament amb tots els fills que es llisten a sota, i no podreu restaurar aquest contingut. Segur que voleu suprimir de manera permanent aquest element?', 'recycle_bin_destroy_list' => 'Elements per destruir', 'recycle_bin_restore_list' => 'Elements per restaurar', 'recycle_bin_restore_confirm' => 'Aquesta acció restaurarà l’element suprimit, incloent-hi els elements fills, a la seva ubicació original. Si la ubicació original s’ha suprimit i és a la paperera, l’element pare també s’haurà de restaurar.', @@ -192,7 +192,7 @@ return [ 'role_access_api' => 'Accés a l’API del sistema', 'role_manage_settings' => 'Gestió de la configuració de l’aplicació', 'role_export_content' => 'Exportació de contingut', - 'role_import_content' => 'Import content', + 'role_import_content' => 'Importar contingut', 'role_editor_change' => 'Canvi de l’editor de pàgina', 'role_notifications' => 'Recepció i gestió de notificacions', 'role_asset' => 'Permisos de recursos', @@ -308,13 +308,13 @@ return [ 'webhooks_last_error_message' => 'Darrer missatge d’error:', // Licensing - 'licenses' => 'Licenses', - 'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.', - 'licenses_bookstack' => 'BookStack License', - 'licenses_php' => 'PHP Library Licenses', - 'licenses_js' => 'JavaScript Library Licenses', - 'licenses_other' => 'Other Licenses', - 'license_details' => 'License Details', + 'licenses' => 'Llicències', + 'licenses_desc' => 'Aquesta pàgina detalla informació sobre la llicència de BookStack a més dels projectes i biblioteques que s\'utilitzen en BookStack. Molts projectes enumerats aquí poden ser utilitzats només en un context de desenvolupament.', + 'licenses_bookstack' => 'Llicència de BookStack', + 'licenses_php' => 'Llicències de Biblioteques de PHP', + 'licenses_js' => 'Llicències de Biblioteques de JavaScript', + 'licenses_other' => 'Altres llicències', + 'license_details' => 'Detalls de la llicència', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. diff --git a/lang/ca/validation.php b/lang/ca/validation.php index e33ae336f..bc821749a 100644 --- a/lang/ca/validation.php +++ b/lang/ca/validation.php @@ -105,10 +105,10 @@ return [ 'url' => 'El format :attribute no és vàlid.', 'uploaded' => 'No s’ha pogut pujar el fitxer. És possible que el servidor no admeti fitxers d’aquesta mida.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => 'El :attribute necessita fer referència a un arxiu dins del ZIP.', + 'zip_file_mime' => 'El :attribute necessita fer referència a un arxiu de tipus :validTyes, trobat :foundType.', + 'zip_model_expected' => 'S\'esperava un objecte de dades, però s\'ha trobat ":type".', + 'zip_unique' => 'El :attribute ha de ser únic pel tipus d\'objecte dins del ZIP.', // Custom validation lines 'custom' => [ diff --git a/lang/da/auth.php b/lang/da/auth.php index 9f717a3d5..dcc531125 100644 --- a/lang/da/auth.php +++ b/lang/da/auth.php @@ -6,11 +6,11 @@ */ return [ - 'failed' => 'Dee indtastede brugeroplysninger stemmer ikke overens med vores registreringer.', + 'failed' => 'De indtastede brugeroplysninger stemmer ikke overens med vores registreringer.', 'throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.', // Login & Register - 'sign_up' => 'Registrér', + 'sign_up' => 'Registrer', 'log_in' => 'Log ind', 'log_in_with' => 'Log ind med :socialDriver', 'sign_up_with' => 'Registrér med :socialDriver', @@ -45,28 +45,28 @@ return [ // Password Reset 'reset_password' => 'Nulstil adgangskode', - 'reset_password_send_instructions' => 'Indtast din E-Mail herunder og du vil blive sendt en E-Mail med et link til at nulstille din adgangskode.', + 'reset_password_send_instructions' => 'Indtast din e-mail herunder og du vil blive sendt en e-mail med et link til at nulstille din adgangskode.', 'reset_password_send_button' => 'Send link til nulstilling', 'reset_password_sent' => 'Et link til nulstilling af adgangskode sendes til :email, hvis den e-mail-adresse findes i systemet.', 'reset_password_success' => 'Din adgangskode er blevet nulstillet.', 'email_reset_subject' => 'Nulstil din :appName adgangskode', - 'email_reset_text' => 'Du modtager denne E-Mail fordi vi har modtaget en anmodning om at nulstille din adgangskode.', + 'email_reset_text' => 'Du modtager denne e-mail fordi vi har modtaget en anmodning om at nulstille din adgangskode.', 'email_reset_not_requested' => 'Hvis du ikke har anmodet om at få din adgangskode nulstillet, behøver du ikke at foretage dig noget.', // Email Confirmation 'email_confirm_subject' => 'Bekræft din E-Mail på :appName', 'email_confirm_greeting' => 'Tak for at oprette dig på :appName!', - 'email_confirm_text' => 'Bekræft venligst din E-Mail adresse ved at klikke på linket nedenfor:', - 'email_confirm_action' => 'Bekræft E-Mail', - 'email_confirm_send_error' => 'E-Mail-bekræftelse kræves, men systemet kunne ikke sende E-Mailen. Kontakt administratoren for at sikre, at E-Mail er konfigureret korrekt.', - 'email_confirm_success' => 'Din email er blevet bekræftet! Du bør nu kune logge ind med denne emailadresse.', + 'email_confirm_text' => 'Bekræft venligst din e-mail adresse ved at klikke på linket nedenfor:', + 'email_confirm_action' => 'Bekræft e-mail', + 'email_confirm_send_error' => 'E-mailbekræftelse kræves, men systemet kunne ikke sende e-mailen. Kontakt administratoren for at sikre, at e-mail er konfigureret korrekt.', + 'email_confirm_success' => 'Din e-mail er blevet bekræftet! Du bør nu kunne logge ind med denne e-mailadresse.', 'email_confirm_resent' => 'Bekræftelsesmail sendt, tjek venligst din indboks.', 'email_confirm_thanks' => 'Tak for bekræftelsen!', 'email_confirm_thanks_desc' => 'Vent venligst et øjeblik, mens din bekræftelse behandles. Hvis du ikke bliver omdirigeret efter 3 sekunder, skal du trykke på linket "Fortsæt" nedenfor for at fortsætte.', - 'email_not_confirmed' => 'E-Mail adresse ikke bekræftet', - 'email_not_confirmed_text' => 'Din E-Mail adresse er endnu ikke blevet bekræftet.', - 'email_not_confirmed_click_link' => 'Klik venligst på linket i E-Mailen der blev sendt kort efter du registrerede dig.', + 'email_not_confirmed' => 'E-mailadresse ikke bekræftet', + 'email_not_confirmed_text' => 'Din e-mailadresse er endnu ikke blevet bekræftet.', + 'email_not_confirmed_click_link' => 'Klik venligst på linket i e-mailen der blev sendt kort efter du registrerede dig.', 'email_not_confirmed_resend' => 'Hvis du ikke kan finde E-Mailen, kan du du få gensendt bekræftelsesemailen ved at trykke herunder.', 'email_not_confirmed_resend_button' => 'Gensend bekræftelsesemail', diff --git a/lang/da/common.php b/lang/da/common.php index 69c80e844..db75772c8 100644 --- a/lang/da/common.php +++ b/lang/da/common.php @@ -30,8 +30,8 @@ return [ 'create' => 'Opret', 'update' => 'Opdater', 'edit' => 'Rediger', - 'archive' => 'Archive', - 'unarchive' => 'Un-Archive', + 'archive' => 'Arkiver', + 'unarchive' => 'Tilbagekald', 'sort' => 'Sorter', 'move' => 'Flyt', 'copy' => 'Kopier', @@ -50,8 +50,8 @@ return [ 'unfavourite' => 'Fjern som foretrukken', 'next' => 'Næste', 'previous' => 'Forrige', - 'filter_active' => 'Aktivt Filter:', - 'filter_clear' => 'Nulstil Filter', + 'filter_active' => 'Aktivt filter:', + 'filter_clear' => 'Nulstil filter', 'download' => 'Hent', 'open_in_tab' => 'Åben i ny fane', 'open' => 'Åbn', diff --git a/lang/da/components.php b/lang/da/components.php index 410ed4fc5..3deeadf48 100644 --- a/lang/da/components.php +++ b/lang/da/components.php @@ -6,8 +6,8 @@ return [ // Image Manager 'image_select' => 'Billedselektion', - 'image_list' => 'Billede Liste', - 'image_details' => 'Billede Detaljer', + 'image_list' => 'Billedliste', + 'image_details' => 'Billeddetaljer', 'image_upload' => 'Upload billede', 'image_intro' => 'Her kan du vælge og administrere billeder, der tidligere er blevet uploadet til systemet.', 'image_intro_upload' => 'Upload et nyt billede ved at trække en billedfil ind i dette vindue, eller ved at bruge knappen "Upload billede" ovenfor.', diff --git a/lang/da/entities.php b/lang/da/entities.php index c91772190..66e3fd206 100644 --- a/lang/da/entities.php +++ b/lang/da/entities.php @@ -272,7 +272,7 @@ return [ 'pages_md_insert_drawing' => 'Indsæt tegning', 'pages_md_show_preview' => 'Vis forhåndsvisning', 'pages_md_sync_scroll' => 'Rulning af forhåndsvisning af synkronisering', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_plain_editor' => 'Klartekst editor', 'pages_drawing_unsaved' => 'Ikke gemt tegning fundet', 'pages_drawing_unsaved_confirm' => 'Der blev fundet ikke-gemte tegningsdata fra et tidligere mislykket forsøg på at gemme en tegning. Vil du gendanne og fortsætte med at redigere denne ikke-gemte tegning?', 'pages_not_in_chapter' => 'Side er ikke i et kapitel', @@ -400,7 +400,7 @@ return [ 'comment_none' => 'No comments to display', 'comment_placeholder' => 'Skriv en kommentar her', 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', + 'comment_archived_count' => ':count Arkiveret', 'comment_archived_threads' => 'Archived Threads', 'comment_save' => 'Gem kommentar', 'comment_new' => 'Ny kommentar', @@ -412,12 +412,12 @@ return [ 'comment_updated_success' => 'Kommentaren er opdateret', 'comment_archive_success' => 'Comment archived', 'comment_unarchive_success' => 'Comment un-archived', - 'comment_view' => 'View comment', - 'comment_jump_to_thread' => 'Jump to thread', + 'comment_view' => 'Se kommentar', + 'comment_jump_to_thread' => 'Hop til tråd', 'comment_delete_confirm' => 'Er du sikker på, at du vil slette denne kommentar?', 'comment_in_reply_to' => 'Som svar til :commentId', 'comment_reference' => 'Reference', - 'comment_reference_outdated' => '(Outdated)', + 'comment_reference_outdated' => '(Forældet)', 'comment_editor_explain' => 'Her er de kommentarer, der er blevet efterladt på denne side. Kommentarer kan tilføjes og administreres, når du ser den gemte side.', // Revision diff --git a/lang/da/passwords.php b/lang/da/passwords.php index 343fa2b85..b58a4c94b 100644 --- a/lang/da/passwords.php +++ b/lang/da/passwords.php @@ -7,7 +7,7 @@ return [ 'password' => 'Adgangskoder skal være mindst otte tegn og svare til bekræftelsen.', - 'user' => "Vi kan ikke finde en bruger med den e-mail adresse.", + 'user' => "Vi kan ikke finde en bruger med den e-mailadresse.", 'token' => 'Linket til nulstilling af adgangskode er ugyldigt for denne e-mail-adresse.', 'sent' => 'Vi har sendt dig en e-mail med et link til at nulstille adgangskoden!', 'reset' => 'Dit kodeord er blevet nulstillet!', diff --git a/lang/da/preferences.php b/lang/da/preferences.php index 9b9436dba..143405a16 100644 --- a/lang/da/preferences.php +++ b/lang/da/preferences.php @@ -24,12 +24,12 @@ return [ 'notifications_opt_own_page_changes' => 'Adviser ved ændringer af sider, jeg ejer', 'notifications_opt_own_page_comments' => 'Adviser ved kommentarer på sider, jeg ejer', 'notifications_opt_comment_replies' => 'Adviser ved svar på mine kommentarer', - 'notifications_save' => 'Gem Indstillinger', + 'notifications_save' => 'Gem indstillinger', 'notifications_update_success' => 'Indstillinger for notifikationer er blevet opdateret!', - 'notifications_watched' => 'Overvågede & Ignorerede', + 'notifications_watched' => 'Overvågede og ignorerede', 'notifications_watched_desc' => 'Nedenfor er de elementer, der har brugerdefinerede overvågning aktivt. For at opdatere dine præferencer for disse, gå til elementet og find derefter overvågning i sidepanelet.', - 'auth' => 'Adgang & Sikkerhed', + 'auth' => 'Adgang og sikkerhed', 'auth_change_password' => 'Skift adgangskode', 'auth_change_password_desc' => 'Skift den adgangskode, du bruger til at logge ind med. Den skal være mindst 8 tegn lang.', 'auth_change_password_success' => 'Adgangskoden er blevet opdateret!', @@ -41,7 +41,7 @@ return [ 'profile_email_desc' => 'Denne e-mail vil blive brugt til notifikationer og, afhængigt af aktiv systemgodkendelse, systemadgang.', 'profile_email_no_permission' => 'Desværre har du ikke tilladelse til at ændre din e-mailadresse. Hvis du ønsker at ændre dette, skal du bede en administrator om at ændre dette for dig.', 'profile_avatar_desc' => 'Vælg et billede som vil blive brugt til at repræsentere dig selv over for andre i systemet. Ideelt set bør dette billede være kvadrat og omkring 256px i bredde og højde.', - 'profile_admin_options' => 'Administrator Indstillinger', + 'profile_admin_options' => 'Administratorindstillinger', 'profile_admin_options_desc' => 'Yderligere indstillinger på administratorniveau, såsom dem der håndterer rolleopgaver, kan findes for din brugerkonto i området "Indstillinger > Brugere".', 'delete_account' => 'Slet konto', diff --git a/lang/da/settings.php b/lang/da/settings.php index a4136f6fd..34d3975be 100644 --- a/lang/da/settings.php +++ b/lang/da/settings.php @@ -26,14 +26,14 @@ return [ 'app_secure_images' => 'Højere sikkerhed for billeduploads', 'app_secure_images_toggle' => 'Aktiver højere sikkerhed for billeduploads', 'app_secure_images_desc' => 'Af performanceårsager er alle billeder offentlige. Denne funktion tilføjer en tilfældig, vanskelig at gætte streng foran billed-url\'er. Sørg for, at mappeindeksering ikke er aktiveret for at forhindre nem adgang.', - 'app_default_editor' => 'Standard Side Editor', + 'app_default_editor' => 'Standard sideeditor', 'app_default_editor_desc' => 'Vælg hvilken editor der som standard skal bruges ved redigering af nye sider. Dette kan tilsidesættes på side niveau, hvor tilladelser tillader det.', 'app_custom_html' => 'Tilpasset HTML head indhold', 'app_custom_html_desc' => 'Alt indhold tilføjet her, vil blive indsat i bunden af sektionen på alle sider. Dette er brugbart til overskrivning af styles og tilføjelse af analytics kode.', 'app_custom_html_disabled_notice' => 'Brugerdefineret HTML head indhold er deaktiveret på denne indstillingsside for at, at ændringer kan rulles tilbage.', 'app_logo' => 'Applikationslogo', 'app_logo_desc' => 'Det bruges blandt andet i applikationens headerbar. Dette billede skal være 86px i højden. Store billeder vil blive skaleret ned.', - 'app_icon' => 'Program ikon', + 'app_icon' => 'Programikon', 'app_icon_desc' => 'Dette ikon bruges til browserfaner og genvejsikoner. Det skal være et 256px kvadratisk PNG-billede.', 'app_homepage' => 'Applikationsforside', 'app_homepage_desc' => 'Vælg en visning, der skal vises på forsiden i stedet for standardvisningen. Sidetilladelser ignoreres for de valgte sider.', diff --git a/lang/et/entities.php b/lang/et/entities.php index 72f09eca0..323985b2d 100644 --- a/lang/et/entities.php +++ b/lang/et/entities.php @@ -63,10 +63,10 @@ return [ 'import_delete_desc' => 'See kustutab üleslaaditud ZIP-faili, ja seda ei saa tagasi võtta.', 'import_errors' => 'Importimise vead', 'import_errors_desc' => 'Importimisel esinesid järgmised vead:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'breadcrumb_siblings_for_page' => 'Sirvi teisi lehti', + 'breadcrumb_siblings_for_chapter' => 'Sirvi teisi peatükke', + 'breadcrumb_siblings_for_book' => 'Sirvi teisi raamatuid', + 'breadcrumb_siblings_for_bookshelf' => 'Sirvi teisi riiuleid', // Permissions and restrictions 'permissions' => 'Õigused', diff --git a/lang/ja/entities.php b/lang/ja/entities.php index eed0c92ca..551a2e496 100644 --- a/lang/ja/entities.php +++ b/lang/ja/entities.php @@ -63,10 +63,10 @@ return [ 'import_delete_desc' => 'アップロードされたインポートZIPファイルは削除され、元に戻すことはできません。', 'import_errors' => 'インポートエラー', 'import_errors_desc' => 'インポート中に次のエラーが発生しました:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'breadcrumb_siblings_for_page' => '階層内のページ', + 'breadcrumb_siblings_for_chapter' => '階層内のチャプタ', + 'breadcrumb_siblings_for_book' => '階層内のブック', + 'breadcrumb_siblings_for_bookshelf' => '階層内の棚', // Permissions and restrictions 'permissions' => '権限', diff --git a/lang/nl/activities.php b/lang/nl/activities.php index 3d548e2a7..230c7a58b 100644 --- a/lang/nl/activities.php +++ b/lang/nl/activities.php @@ -122,7 +122,7 @@ return [ 'recycle_bin_destroy' => 'verwijderde van prullenbak', // Comments - 'commented_on' => 'reageerde op', + 'commented_on' => 'plaatste opmerking in', 'comment_create' => 'voegde opmerking toe', 'comment_update' => 'paste opmerking aan', 'comment_delete' => 'verwijderde opmerking', diff --git a/lang/nl/common.php b/lang/nl/common.php index 47d8703ca..bcd7fbe08 100644 --- a/lang/nl/common.php +++ b/lang/nl/common.php @@ -40,7 +40,7 @@ return [ 'delete_confirm' => 'Verwijdering bevestigen', 'search' => 'Zoek', 'search_clear' => 'Zoekopdracht wissen', - 'reset' => 'Reset', + 'reset' => 'Wissen', 'remove' => 'Verwijder', 'add' => 'Voeg toe', 'configure' => 'Configureer', diff --git a/lang/nl/editor.php b/lang/nl/editor.php index 0963df71f..cc3ad108c 100644 --- a/lang/nl/editor.php +++ b/lang/nl/editor.php @@ -48,7 +48,7 @@ return [ 'superscript' => 'Superscript', 'subscript' => 'Subscript', 'text_color' => 'Tekstkleur', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Accentkleur', 'custom_color' => 'Aangepaste kleur', 'remove_color' => 'Verwijder kleur', 'background_color' => 'Tekstmarkeringskleur', diff --git a/lang/nl/entities.php b/lang/nl/entities.php index 967b7ecfb..b9be1f42b 100644 --- a/lang/nl/entities.php +++ b/lang/nl/entities.php @@ -63,10 +63,10 @@ return [ 'import_delete_desc' => 'Dit zal het Zip-bestand van de import permanent verwijderen.', 'import_errors' => 'Importeerfouten', 'import_errors_desc' => 'De volgende fouten deden zich voor tijdens het importeren:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'breadcrumb_siblings_for_page' => 'Navigeer pagina\'s op hetzelfde niveau', + 'breadcrumb_siblings_for_chapter' => 'Navigeer hoofdstukken op hetzelfde niveau', + 'breadcrumb_siblings_for_book' => 'Navigeer boeken op hetzelfde niveau', + 'breadcrumb_siblings_for_bookshelf' => 'Navigeer boekenplanken op hetzelfde niveau', // Permissions and restrictions 'permissions' => 'Machtigingen', @@ -272,11 +272,11 @@ return [ 'pages_md_insert_drawing' => 'Tekening invoegen', 'pages_md_show_preview' => 'Toon voorbeeld', 'pages_md_sync_scroll' => 'Synchroniseer scrollen van voorbeeld', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_plain_editor' => 'Normale tekst bewerker', 'pages_drawing_unsaved' => 'Niet-opgeslagen Tekening Gevonden', 'pages_drawing_unsaved_confirm' => 'Er zijn niet-opgeslagen tekeninggegevens gevonden van een eerdere mislukte poging om de tekening op te slaan. Wil je deze niet-opgeslagen tekening herstellen en verder bewerken?', 'pages_not_in_chapter' => 'Pagina is niet in een hoofdstuk', - 'pages_move' => 'Pagina verplaatsten', + 'pages_move' => 'Pagina Verplaatsen', 'pages_copy' => 'Pagina kopiëren', 'pages_copy_desination' => 'Kopieër bestemming', 'pages_copy_success' => 'Pagina succesvol gekopieerd', @@ -394,27 +394,27 @@ return [ 'profile_not_created_shelves' => ':userName heeft geen boekenplanken gemaakt', // Comments - 'comment' => 'Reactie', - 'comments' => 'Reacties', - 'comment_add' => 'Reactie toevoegen', + 'comment' => 'Opmerking', + 'comments' => 'Opmerkingen', + 'comment_add' => 'Opmerking toevoegen', 'comment_none' => 'Geen opmerkingen om weer te geven', - 'comment_placeholder' => 'Laat hier een reactie achter', - 'comment_thread_count' => ':count Reactie Thread|:count Reactie Threads', + 'comment_placeholder' => 'Laat hier een opmerking achter', + 'comment_thread_count' => ':count Opmerking Thread|:count Opmerking Threads', 'comment_archived_count' => ':count Gearchiveerd', 'comment_archived_threads' => 'Gearchiveerde Threads', - 'comment_save' => 'Sla reactie op', - 'comment_new' => 'Nieuwe reactie', - 'comment_created' => 'reactie gegeven :createDiff', + 'comment_save' => 'Sla opmerking op', + 'comment_new' => 'Nieuwe opmerking', + 'comment_created' => 'opmerking gegeven :createDiff', 'comment_updated' => 'Updatet :updateDiff door :username', 'comment_updated_indicator' => 'Bijgewerkt', - 'comment_deleted_success' => 'Reactie verwijderd', - 'comment_created_success' => 'Reactie toegevoegd', - 'comment_updated_success' => 'Reactie bijgewerkt', + 'comment_deleted_success' => 'Opmerking verwijderd', + 'comment_created_success' => 'Opmerking toegevoegd', + 'comment_updated_success' => 'Opmerking bijgewerkt', 'comment_archive_success' => 'Opmerking gearchiveerd', 'comment_unarchive_success' => 'Opmerking teruggehaald', 'comment_view' => 'Opmerking weergeven', 'comment_jump_to_thread' => 'Ga naar thread', - 'comment_delete_confirm' => 'Weet je zeker dat je deze reactie wilt verwijderen?', + 'comment_delete_confirm' => 'Weet je zeker dat je deze opmerking wilt verwijderen?', 'comment_in_reply_to' => 'Als antwoord op :commentId', 'comment_reference' => 'Verwijzing', 'comment_reference_outdated' => '(Verouderd)', @@ -466,11 +466,11 @@ return [ 'watch_desc_comments_page' => 'Geef een melding van pagina wijzigingen en nieuwe opmerkingen.', 'watch_change_default' => 'Standaardvoorkeuren voor meldingen wijzigen', 'watch_detail_ignore' => 'Meldingen negeren', - 'watch_detail_new' => 'Op de uitkijk voor nieuwe pagina\'s', - 'watch_detail_updates' => 'Op de uitkijk voor nieuwe pagina\'s en aanpassingen', - 'watch_detail_comments' => 'Op de uitkijk voor nieuwe pagina\'s, aanpassingen en opmerkingen', - 'watch_detail_parent_book' => 'Op de uitkijk via hogerliggend boek', - 'watch_detail_parent_book_ignore' => 'Aan het negeren via hogerliggend boek', - 'watch_detail_parent_chapter' => 'Op de uitkijk via hogerliggend hoofdstuk', - 'watch_detail_parent_chapter_ignore' => 'Aan het negeren via hogerliggend hoofdstuk', + 'watch_detail_new' => 'Nieuwe pagina\'s aan het volgen', + 'watch_detail_updates' => 'Nieuwe pagina\'s en aanpassingen aan het volgen', + 'watch_detail_comments' => 'Nieuwe pagina\'s, aanpassingen en opmerkingen aan het volgen', + 'watch_detail_parent_book' => 'Aan het volgen via bovenliggend boek', + 'watch_detail_parent_book_ignore' => 'Aan het negeren via bovenliggend boek', + 'watch_detail_parent_chapter' => 'Aan het volgen via bovenliggend hoofdstuk', + 'watch_detail_parent_chapter_ignore' => 'Aan het negeren via bovenliggend hoofdstuk', ]; diff --git a/lang/nl/errors.php b/lang/nl/errors.php index d8d5af723..9829986a2 100644 --- a/lang/nl/errors.php +++ b/lang/nl/errors.php @@ -87,11 +87,11 @@ return [ 'role_cannot_remove_only_admin' => 'Deze gebruiker is de enige gebruiker die is toegewezen aan de beheerdersrol. Wijs de beheerdersrol toe aan een andere gebruiker voordat u probeert deze hier te verwijderen.', // Comments - 'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.', - 'cannot_add_comment_to_draft' => 'Je kunt geen reacties toevoegen aan een concept.', - 'comment_add' => 'Er is een fout opgetreden tijdens het aanpassen / toevoegen van de reactie.', - 'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.', - 'empty_comment' => 'Kan geen lege reactie toevoegen.', + 'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de opmerkingen.', + 'cannot_add_comment_to_draft' => 'Je kunt geen opmerkingen toevoegen aan een concept.', + 'comment_add' => 'Er is een fout opgetreden tijdens het aanpassen / toevoegen van de opmerking.', + 'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de opmerking.', + 'empty_comment' => 'Kan geen lege opmerking toevoegen.', // Error pages '404_page_not_found' => 'Pagina Niet Gevonden', diff --git a/lang/nl/notifications.php b/lang/nl/notifications.php index bc5f380c3..1e7035670 100644 --- a/lang/nl/notifications.php +++ b/lang/nl/notifications.php @@ -5,7 +5,7 @@ return [ 'new_comment_subject' => 'Nieuwe opmerking op pagina: :pageName', - 'new_comment_intro' => 'Een gebruiker heeft gereageerd op een pagina in :appName:', + 'new_comment_intro' => 'Een gebruiker heeft een opmerking geplaatst op een pagina in :appName:', 'new_page_subject' => 'Nieuwe pagina: :pageName', 'new_page_intro' => 'Een nieuwe pagina is gemaakt in :appName:', 'updated_page_subject' => 'Aangepaste pagina: :pageName', @@ -14,7 +14,7 @@ return [ 'detail_page_name' => 'Pagina Naam:', 'detail_page_path' => 'Paginapad:', - 'detail_commenter' => 'Reageerder:', + 'detail_commenter' => 'Commentator:', 'detail_comment' => 'Opmerking:', 'detail_created_by' => 'Gemaakt Door:', 'detail_updated_by' => 'Aangepast Door:', diff --git a/lang/nl/preferences.php b/lang/nl/preferences.php index 5e385276c..d0e3623b9 100644 --- a/lang/nl/preferences.php +++ b/lang/nl/preferences.php @@ -27,7 +27,7 @@ return [ 'notifications_save' => 'Voorkeuren opslaan', 'notifications_update_success' => 'Voorkeuren voor meldingen zijn bijgewerkt!', 'notifications_watched' => 'Gevolgde & Genegeerde Items', - 'notifications_watched_desc' => 'Hieronder staan de items waarvoor aangepaste \'Volg\'-voorkeuren zijn toegepast. Om je voorkeuren voor deze items bij te werken, bekijk je het item en zoek je naar de \'Volg\' opties in de zijbalk.', + 'notifications_watched_desc' => 'Hieronder staan de items waarvoor aangepaste volgvoorkeuren zijn toegepast. Om je voorkeuren voor deze items bij te werken, bekijk je het item en zoek je naar de volgopties in de zijbalk.', 'auth' => 'Toegang & Beveiliging', 'auth_change_password' => 'Wachtwoord Wijzigen', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 1aeabed10..3b4da75a9 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -43,9 +43,9 @@ return [ 'app_footer_links_label' => 'Link label', 'app_footer_links_url' => 'Link URL', 'app_footer_links_add' => 'Voettekst link toevoegen', - 'app_disable_comments' => 'Reacties uitschakelen', - 'app_disable_comments_toggle' => 'Reacties uitschakelen', - 'app_disable_comments_desc' => 'Schakel reacties uit op alle pagina\'s in de applicatie.
Bestaande reacties worden niet getoond.', + 'app_disable_comments' => 'Opmerkingen uitschakelen', + 'app_disable_comments_toggle' => 'Opmerkingen uitschakelen', + 'app_disable_comments_desc' => 'Schakel opmerkingen uit op alle pagina\'s in de applicatie.
Bestaande opmerkingen worden niet getoond.', // Color settings 'color_scheme' => 'Kleurenschema van applicatie', diff --git a/lang/ru/editor.php b/lang/ru/editor.php index bd0c45f31..d26f7edfe 100644 --- a/lang/ru/editor.php +++ b/lang/ru/editor.php @@ -29,10 +29,10 @@ return [ // Toolbar 'formats' => 'Форматы', - 'header_large' => 'Большой', - 'header_medium' => 'Средний', - 'header_small' => 'Маленький', - 'header_tiny' => 'Крошечный', + 'header_large' => 'Крупный заголовок', + 'header_medium' => 'Средний заголовок', + 'header_small' => 'Небольшой заголовок', + 'header_tiny' => 'Маленький заголовок', 'paragraph' => 'Обычный текст', 'blockquote' => 'Цитата', 'inline_code' => 'Встроенный код', diff --git a/lang/uk/common.php b/lang/uk/common.php index 8539a93cf..d71f57162 100644 --- a/lang/uk/common.php +++ b/lang/uk/common.php @@ -30,8 +30,8 @@ return [ 'create' => 'Створити', 'update' => 'Оновити', 'edit' => 'Редагувати', - 'archive' => 'Archive', - 'unarchive' => 'Un-Archive', + 'archive' => 'Архів', + 'unarchive' => 'Розархівувати', 'sort' => 'Сортувати', 'move' => 'Перемістити', 'copy' => 'Копіювати', diff --git a/lang/uk/editor.php b/lang/uk/editor.php index 6dbeb509b..b9024e052 100644 --- a/lang/uk/editor.php +++ b/lang/uk/editor.php @@ -48,7 +48,7 @@ return [ 'superscript' => 'Верхній індекс', 'subscript' => 'Нижній індекс', 'text_color' => 'Колір тексту', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Колір підсвічування', 'custom_color' => 'Власний колір', 'remove_color' => 'Видалити колір', 'background_color' => 'Колір фону', diff --git a/lang/uk/entities.php b/lang/uk/entities.php index 72853697b..cf79b530c 100644 --- a/lang/uk/entities.php +++ b/lang/uk/entities.php @@ -63,10 +63,10 @@ return [ 'import_delete_desc' => 'Це видалить завантажений імпорт файлу ZIP, і його не можна буде скасувати.', 'import_errors' => 'Помилки імпорту', 'import_errors_desc' => 'Під час спроби імпорту відбулися наступні помилки:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'breadcrumb_siblings_for_page' => 'Переглянути інші сторінки', + 'breadcrumb_siblings_for_chapter' => 'Переглянути інші розділи', + 'breadcrumb_siblings_for_book' => 'Переглянути інші книги', + 'breadcrumb_siblings_for_bookshelf' => 'Переглянути інші полиці', // Permissions and restrictions 'permissions' => 'Дозволи', @@ -252,7 +252,7 @@ return [ 'pages_edit_switch_to_markdown_stable' => '(Стабілізувати вміст)', 'pages_edit_switch_to_wysiwyg' => 'Змінити редактор на WYSIWYG', 'pages_edit_switch_to_new_wysiwyg' => 'Перейти на новий WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(На бета-тестування)', 'pages_edit_set_changelog' => 'Встановити журнал змін', 'pages_edit_enter_changelog_desc' => 'Введіть короткий опис внесених вами змін', 'pages_edit_enter_changelog' => 'Введіть список змін', @@ -272,7 +272,7 @@ return [ 'pages_md_insert_drawing' => 'Вставити малюнок', 'pages_md_show_preview' => 'Показати попередній перегляд', 'pages_md_sync_scroll' => 'Синхронізація прокручування попереднього перегляду', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_plain_editor' => 'Текстовий редактор', 'pages_drawing_unsaved' => 'Знайдено незбережену чернетку', 'pages_drawing_unsaved_confirm' => 'Незбережені чернетки були знайдені з попередньої спроби зберегти звіт. Хочете відновити і продовжити редагування цієї чернетки?', 'pages_not_in_chapter' => 'Сторінка не знаходиться в розділі', @@ -397,11 +397,11 @@ return [ 'comment' => 'Коментар', 'comments' => 'Коментарі', 'comment_add' => 'Додати коментар', - 'comment_none' => 'No comments to display', + 'comment_none' => 'Немає коментарів для відображення', 'comment_placeholder' => 'Залиште коментар тут', - 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', - 'comment_archived_threads' => 'Archived Threads', + 'comment_thread_count' => ':count тема коментарів|:count теми коментарів', + 'comment_archived_count' => 'Архівовано :count', + 'comment_archived_threads' => 'Архівовані теми', 'comment_save' => 'Зберегти коментар', 'comment_new' => 'Новий коментар', 'comment_created' => 'прокоментував :createDiff', @@ -410,14 +410,14 @@ return [ 'comment_deleted_success' => 'Коментар видалено', 'comment_created_success' => 'Коментар додано', 'comment_updated_success' => 'Коментар оновлено', - 'comment_archive_success' => 'Comment archived', - 'comment_unarchive_success' => 'Comment un-archived', - 'comment_view' => 'View comment', - 'comment_jump_to_thread' => 'Jump to thread', + 'comment_archive_success' => 'Коментар архівовано', + 'comment_unarchive_success' => 'Коментар розархівовано', + 'comment_view' => 'Переглянути коментар', + 'comment_jump_to_thread' => 'Перейти до теми', 'comment_delete_confirm' => 'Ви впевнені, що хочете видалити цей коментар?', 'comment_in_reply_to' => 'У відповідь на :commentId', - 'comment_reference' => 'Reference', - 'comment_reference_outdated' => '(Outdated)', + 'comment_reference' => 'По посиланню', + 'comment_reference_outdated' => '(Застарілий)', 'comment_editor_explain' => 'Ось коментарі, які залишилися на цій сторінці. Коментарі можна додати та керовані при перегляді збереженої сторінки.', // Revision diff --git a/lang/vi/editor.php b/lang/vi/editor.php index d76a70815..fd3dbdfb1 100644 --- a/lang/vi/editor.php +++ b/lang/vi/editor.php @@ -48,7 +48,7 @@ return [ 'superscript' => 'Chỉ số trên', 'subscript' => 'Chỉ số dưới', 'text_color' => 'Màu chữ', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Màu đánh dấu', 'custom_color' => 'Màu tùy chỉnh', 'remove_color' => 'Xóa màu', 'background_color' => 'Màu nền', diff --git a/lang/vi/entities.php b/lang/vi/entities.php index 1914d32c8..f538f993c 100644 --- a/lang/vi/entities.php +++ b/lang/vi/entities.php @@ -60,13 +60,13 @@ return [ 'import_location' => 'Vị trí nhập', 'import_location_desc' => 'Chọn vị trí đích cho nội dung đã nhập của bạn. Bạn sẽ cần các quyền liên quan để tạo trong vị trí bạn chọn.', 'import_delete_confirm' => 'Bạn có chắc chắn muốn xóa lượt nhập này không?', - 'import_delete_desc' => 'Thao tác này sẽ xóa tệp ZIP nhập đã tải lên và không thể hoàn tác.', - 'import_errors' => 'Lỗi nhập', + 'import_delete_desc' => 'Thao tác này sẽ xóa tệp ZIP đã tải lên, và không thể hoàn tác.', + 'import_errors' => 'Lỗi khi nhập dữ liệu', 'import_errors_desc' => 'Các lỗi sau đã xảy ra trong quá trình nhập:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'breadcrumb_siblings_for_page' => 'Điều hướng anh chị em cho trang', + 'breadcrumb_siblings_for_chapter' => 'Điều hướng anh chị em cho chương', + 'breadcrumb_siblings_for_book' => 'Điều hướng anh chị em cho sách', + 'breadcrumb_siblings_for_bookshelf' => 'Điều hướng anh chị em cho kệ sách', // Permissions and restrictions 'permissions' => 'Quyền', From 955837c9aa16e72f3e701bd8737e05c8ae715b26 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 Aug 2025 15:02:26 +0100 Subject: [PATCH 013/139] Packages: Upgraded php deps to latest versions --- composer.lock | 202 +++++++++++++++++++++++++++++--------------------- 1 file changed, 117 insertions(+), 85 deletions(-) diff --git a/composer.lock b/composer.lock index 54ea99275..fe7bfbb38 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.354.0", + "version": "3.356.6", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "014ce3465277cf78a05e60c04ce04c9893733bf2" + "reference": "079d1936bef94cb7d76113b62e993f2996db6e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/014ce3465277cf78a05e60c04ce04c9893733bf2", - "reference": "014ce3465277cf78a05e60c04ce04c9893733bf2", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/079d1936bef94cb7d76113b62e993f2996db6e24", + "reference": "079d1936bef94cb7d76113b62e993f2996db6e24", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.354.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.356.6" }, - "time": "2025-08-14T18:10:08+00:00" + "time": "2025-08-27T18:06:10+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1117,22 +1117,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -1223,7 +1223,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -1239,20 +1239,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -1260,7 +1260,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -1306,7 +1306,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -1322,20 +1322,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -1351,7 +1351,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -1422,7 +1422,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -1438,20 +1438,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.5", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", "shasum": "" }, "require": { @@ -1460,7 +1460,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1508,7 +1508,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" }, "funding": [ { @@ -1524,7 +1524,7 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2025-08-22T14:27:06+00:00" }, { "name": "intervention/gif", @@ -3866,16 +3866,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.3", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", - "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "shasum": "" }, "require": { @@ -3883,7 +3883,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" }, "type": "library", "extra": { @@ -3925,7 +3925,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" }, "funding": [ { @@ -3937,7 +3937,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:41:07+00:00" + "time": "2025-08-21T11:53:16+00:00" }, { "name": "phpseclib/phpseclib", @@ -6311,7 +6311,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -6370,7 +6370,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -6381,6 +6381,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6390,16 +6394,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -6448,7 +6452,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -6459,16 +6463,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -6531,7 +6539,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" }, "funding": [ { @@ -6542,6 +6550,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6551,7 +6563,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -6612,7 +6624,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -6623,6 +6635,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6632,7 +6648,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -6693,7 +6709,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -6704,6 +6720,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6713,7 +6733,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -6773,7 +6793,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -6784,6 +6804,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -6793,16 +6817,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -6849,7 +6873,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -6860,16 +6884,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -6928,7 +6956,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" }, "funding": [ { @@ -6939,6 +6967,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -8182,16 +8214,16 @@ }, { "name": "larastan/larastan", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "6431d010dd383a9279eb8874a76ddb571738564a" + "reference": "3c223047e374befd1b64959784685d6ecccf66aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/6431d010dd383a9279eb8874a76ddb571738564a", - "reference": "6431d010dd383a9279eb8874a76ddb571738564a", + "url": "https://api.github.com/repos/larastan/larastan/zipball/3c223047e374befd1b64959784685d6ecccf66aa", + "reference": "3c223047e374befd1b64959784685d6ecccf66aa", "shasum": "" }, "require": { @@ -8259,7 +8291,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.6.0" + "source": "https://github.com/larastan/larastan/tree/v3.6.1" }, "funding": [ { @@ -8267,7 +8299,7 @@ "type": "github" } ], - "time": "2025-07-11T06:52:52+00:00" + "time": "2025-08-25T07:24:56+00:00" }, { "name": "mockery/mockery", @@ -8689,16 +8721,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.10", + "version": "11.0.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", - "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", "shasum": "" }, "require": { @@ -8755,7 +8787,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" }, "funding": [ { @@ -8775,7 +8807,7 @@ "type": "tidelift" } ], - "time": "2025-06-18T08:56:18+00:00" + "time": "2025-08-27T14:37:49+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9024,16 +9056,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.32", + "version": "11.5.35", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "101e132dcf9e74a1eb3a309b4f686114ae8f7f36" + "reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/101e132dcf9e74a1eb3a309b4f686114ae8f7f36", - "reference": "101e132dcf9e74a1eb3a309b4f686114ae8f7f36", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d341ee94ee5007b286fc7907b383aae6b5b3cc91", + "reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91", "shasum": "" }, "require": { @@ -9047,7 +9079,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.10", + "phpunit/php-code-coverage": "^11.0.11", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", @@ -9105,7 +9137,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.32" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.35" }, "funding": [ { @@ -9129,7 +9161,7 @@ "type": "tidelift" } ], - "time": "2025-08-12T07:32:49+00:00" + "time": "2025-08-28T05:13:54+00:00" }, { "name": "sebastian/cli-parser", From 481f35606881432fbb1a73dad55e4cc66bf40917 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 Aug 2025 17:39:10 +0100 Subject: [PATCH 014/139] Updated translator & dependency attribution before release v25.07.2 --- .github/translators.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/translators.txt b/.github/translators.txt index bcedaaf62..cd0806205 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -503,3 +503,5 @@ Firr (FirrV) :: Russian João Faro (FaroJoaoFaro) :: Portuguese Danilo dos Santos Barbosa (bozochegou) :: Portuguese, Brazilian Chris (furesoft) :: German +Silvia Isern (eiendragon) :: Catalan +Dennis Kron Pedersen (ahjdp) :: Danish From cdbac63b401a74ca53a307e78e86da4b02b33c6e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 30 Aug 2025 11:10:11 +0100 Subject: [PATCH 015/139] Framework: Updated to Laravel 12 --- app/Config/cache.php | 2 +- app/Config/database.php | 6 +- composer.json | 4 +- composer.lock | 392 +++++++++++++++++++++++++++++----------- 4 files changed, 288 insertions(+), 116 deletions(-) diff --git a/app/Config/cache.php b/app/Config/cache.php index 9a0be8eab..01c822a65 100644 --- a/app/Config/cache.php +++ b/app/Config/cache.php @@ -85,6 +85,6 @@ return [ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'), + 'prefix' => env('CACHE_PREFIX', 'bookstack_cache_'), ]; diff --git a/app/Config/database.php b/app/Config/database.php index 8d38a86df..5edafa777 100644 --- a/app/Config/database.php +++ b/app/Config/database.php @@ -75,7 +75,7 @@ return [ 'collation' => 'utf8mb4_unicode_ci', // Prefixes are only semi-supported and may be unstable // since they are not tested as part of our automated test suite. - // If used, the prefix should not be changed otherwise you will likely receive errors. + // If used, the prefix should not be changed; otherwise you will likely receive errors. 'prefix' => env('DB_TABLE_PREFIX', ''), 'prefix_indexes' => true, 'strict' => false, @@ -103,9 +103,7 @@ return [ ], // Migration Repository Table - // This table keeps track of all the migrations that have already run for - // your application. Using this information, we can determine which of - // the migrations on disk haven't actually been run in the database. + // This table keeps track of all the migrations that have already run for the application. 'migrations' => 'migrations', // Redis configuration to use if set diff --git a/composer.json b/composer.json index 2ea1d802b..52fc4581b 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "guzzlehttp/guzzle": "^7.4", "intervention/image": "^3.5", "knplabs/knp-snappy": "^1.5", - "laravel/framework": "^v11.37", + "laravel/framework": "^v12.26.4", "laravel/socialite": "^5.10", "laravel/tinker": "^2.8", "league/commonmark": "^2.3", @@ -44,7 +44,7 @@ "fakerphp/faker": "^1.21", "itsgoingd/clockwork": "^5.1", "mockery/mockery": "^1.5", - "nunomaduro/collision": "^8.1", + "nunomaduro/collision": "^8.6", "larastan/larastan": "^v3.0", "phpunit/phpunit": "^11.5", "squizlabs/php_codesniffer": "^3.7", diff --git a/composer.lock b/composer.lock index fe7bfbb38..2c7d2cede 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b7695cb9945ec550970c67da96934daf", + "content-hash": "e5783454db9e549054438a448bcc9570", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.356.6", + "version": "3.356.8", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "079d1936bef94cb7d76113b62e993f2996db6e24" + "reference": "3efa8c62c11fedb17b90f60b2d3a9f815b406e63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/079d1936bef94cb7d76113b62e993f2996db6e24", - "reference": "079d1936bef94cb7d76113b62e993f2996db6e24", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3efa8c62c11fedb17b90f60b2d3a9f815b406e63", + "reference": "3efa8c62c11fedb17b90f60b2d3a9f815b406e63", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.356.6" + "source": "https://github.com/aws/aws-sdk-php/tree/3.356.8" }, - "time": "2025-08-27T18:06:10+00:00" + "time": "2025-08-29T18:06:18+00:00" }, { "name": "bacon/bacon-qr-code", @@ -213,16 +213,16 @@ }, { "name": "brick/math", - "version": "0.12.3", + "version": "0.13.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", - "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", "shasum": "" }, "require": { @@ -261,7 +261,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.3" + "source": "https://github.com/brick/math/tree/0.13.1" }, "funding": [ { @@ -269,7 +269,7 @@ "type": "github" } ], - "time": "2025-02-28T13:11:00+00:00" + "time": "2025-03-29T13:50:30+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -1739,20 +1739,20 @@ }, { "name": "laravel/framework", - "version": "v11.45.2", + "version": "v12.26.4", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d134bf11e2208c0c5bd488cf19e612ca176b820a" + "reference": "085a367a32ba86fcfa647bfc796098ae6f795b09" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d134bf11e2208c0c5bd488cf19e612ca176b820a", - "reference": "d134bf11e2208c0c5bd488cf19e612ca176b820a", + "url": "https://api.github.com/repos/laravel/framework/zipball/085a367a32ba86fcfa647bfc796098ae6f795b09", + "reference": "085a367a32ba86fcfa647bfc796098ae6f795b09", "shasum": "" }, "require": { - "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "brick/math": "^0.11|^0.12|^0.13", "composer-runtime-api": "^2.2", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.4", @@ -1767,32 +1767,34 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8.2", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", + "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", "league/commonmark": "^2.7", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.72.6|^3.8.4", + "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^7.0.3", - "symfony/error-handler": "^7.0.3", - "symfony/finder": "^7.0.3", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", "symfony/http-foundation": "^7.2.0", - "symfony/http-kernel": "^7.0.3", - "symfony/mailer": "^7.0.3", - "symfony/mime": "^7.0.3", - "symfony/polyfill-php83": "^1.31", - "symfony/process": "^7.0.3", - "symfony/routing": "^7.0.3", - "symfony/uid": "^7.0.3", - "symfony/var-dumper": "^7.0.3", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.6.1", "voku/portable-ascii": "^2.0.2" @@ -1856,17 +1858,17 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^9.16.0", - "pda/pheanstalk": "^5.0.6", + "orchestra/testbench-core": "^10.6.3", + "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1", - "predis/predis": "^2.3", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0", - "symfony/cache": "^7.0.3", - "symfony/http-client": "^7.0.3", - "symfony/psr-http-message-bridge": "^7.0.3", - "symfony/translation": "^7.0.3" + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", @@ -1892,22 +1894,22 @@ "mockery/mockery": "Required to use mocking (^1.6).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "12.x-dev" } }, "autoload": { @@ -1950,7 +1952,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-08-13T20:28:00+00:00" + "time": "2025-08-29T14:15:53+00:00" }, { "name": "laravel/prompts", @@ -5403,16 +5405,16 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", "shasum": "" }, "require": { @@ -5477,7 +5479,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v7.3.3" }, "funding": [ { @@ -5497,7 +5499,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "symfony/css-selector", @@ -5714,16 +5716,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", "shasum": "" }, "require": { @@ -5774,7 +5776,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" }, "funding": [ { @@ -5785,12 +5787,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-22T09:11:45+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5938,16 +5944,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6" + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6877c122b3a6cc3695849622720054f6e6fa5fa6", - "reference": "6877c122b3a6cc3695849622720054f6e6fa5fa6", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", "shasum": "" }, "require": { @@ -5997,7 +6003,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" }, "funding": [ { @@ -6017,20 +6023,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-20T08:04:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c" + "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6ecc895559ec0097e221ed2fd5eb44d5fede083c", - "reference": "6ecc895559ec0097e221ed2fd5eb44d5fede083c", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b", + "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b", "shasum": "" }, "require": { @@ -6115,7 +6121,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.3" }, "funding": [ { @@ -6135,20 +6141,20 @@ "type": "tidelift" } ], - "time": "2025-07-31T10:45:04+00:00" + "time": "2025-08-29T08:23:45+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b" + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/d43e84d9522345f96ad6283d5dfccc8c1cfc299b", - "reference": "d43e84d9522345f96ad6283d5dfccc8c1cfc299b", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575", + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575", "shasum": "" }, "require": { @@ -6199,7 +6205,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.2" + "source": "https://github.com/symfony/mailer/tree/v7.3.3" }, "funding": [ { @@ -6219,7 +6225,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/mime", @@ -6895,6 +6901,166 @@ ], "time": "2025-07-08T02:45:35+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, { "name": "symfony/polyfill-uuid", "version": "v1.33.0", @@ -6980,16 +7146,16 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", "shasum": "" }, "require": { @@ -7021,7 +7187,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v7.3.3" }, "funding": [ { @@ -7032,12 +7198,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-08-18T09:42:54+00:00" }, { "name": "symfony/routing", @@ -7209,16 +7379,16 @@ }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", "shasum": "" }, "require": { @@ -7276,7 +7446,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v7.3.3" }, "funding": [ { @@ -7296,20 +7466,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-08-25T06:35:40+00:00" }, { "name": "symfony/translation", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90" + "reference": "e0837b4cbcef63c754d89a4806575cada743a38d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/81b48f4daa96272efcce9c7a6c4b58e629df3c90", - "reference": "81b48f4daa96272efcce9c7a6c4b58e629df3c90", + "url": "https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d", + "reference": "e0837b4cbcef63c754d89a4806575cada743a38d", "shasum": "" }, "require": { @@ -7376,7 +7546,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.2" + "source": "https://github.com/symfony/translation/tree/v7.3.3" }, "funding": [ { @@ -7396,7 +7566,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:31:46+00:00" + "time": "2025-08-01T21:02:37+00:00" }, { "name": "symfony/translation-contracts", @@ -7552,16 +7722,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.2", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "53205bea27450dc5c65377518b3275e126d45e75" + "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", - "reference": "53205bea27450dc5c65377518b3275e126d45e75", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", "shasum": "" }, "require": { @@ -7615,7 +7785,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" }, "funding": [ { @@ -7635,7 +7805,7 @@ "type": "tidelift" } ], - "time": "2025-07-29T20:02:46+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -10319,16 +10489,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v7.3.1", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "8b2ee2e06ab99fa5f067b6699296d4e35c156bb9" + "reference": "efa076ea0eeff504383ff0dcf827ea5ce15690ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/8b2ee2e06ab99fa5f067b6699296d4e35c156bb9", - "reference": "8b2ee2e06ab99fa5f067b6699296d4e35c156bb9", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/efa076ea0eeff504383ff0dcf827ea5ce15690ba", + "reference": "efa076ea0eeff504383ff0dcf827ea5ce15690ba", "shasum": "" }, "require": { @@ -10366,7 +10536,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.3.1" + "source": "https://github.com/symfony/dom-crawler/tree/v7.3.3" }, "funding": [ { @@ -10377,12 +10547,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-15T10:07:06+00:00" + "time": "2025-08-06T20:13:54+00:00" }, { "name": "theseer/tokenizer", From 64b06bcf618455f348648aeac2cf1f295726e955 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 30 Aug 2025 11:47:22 +0100 Subject: [PATCH 016/139] Packages: Updated predis --- composer.json | 2 +- composer.lock | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 52fc4581b..2285a22cb 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "onelogin/php-saml": "^4.0", "phpseclib/phpseclib": "^3.0", "pragmarx/google2fa": "^8.0", - "predis/predis": "^2.1", + "predis/predis": "^3.2", "socialiteproviders/discord": "^4.1", "socialiteproviders/gitlab": "^4.1", "socialiteproviders/microsoft-azure": "^5.1", diff --git a/composer.lock b/composer.lock index 2c7d2cede..1dbb75203 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e5783454db9e549054438a448bcc9570", + "content-hash": "506bcb9f80a5fd736b79c8e138efafa0", "packages": [ { "name": "aws/aws-crt-php", @@ -4105,26 +4105,27 @@ }, { "name": "predis/predis", - "version": "v2.4.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b" + "reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/f49e13ee3a2a825631562aa0223ac922ec5d058b", - "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b", + "url": "https://api.github.com/repos/predis/predis/zipball/9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8", + "reference": "9e9deec4dfd3ebf65d32eb368f498c646ba2ecd8", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0|^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.3", "phpstan/phpstan": "^1.9", "phpunit/phpcov": "^6.0 || ^8.0", - "phpunit/phpunit": "^8.0 || ^9.4" + "phpunit/phpunit": "^8.0 || ~9.4.4" }, "suggest": { "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" @@ -4155,7 +4156,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.4.0" + "source": "https://github.com/predis/predis/tree/v3.2.0" }, "funding": [ { @@ -4163,7 +4164,7 @@ "type": "github" } ], - "time": "2025-04-30T15:16:02+00:00" + "time": "2025-08-06T06:41:24+00:00" }, { "name": "psr/clock", From a27ce6e9154387ac24ad3d3df3321ddb19dd7592 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 30 Aug 2025 22:18:09 +0100 Subject: [PATCH 017/139] Packages: Updated npm packages Spent way too many hours debugging through issues from jsdom changes. --- package-lock.json | 3179 ++++++++++------- package.json | 46 +- .../wysiwyg/lexical/core/LexicalSelection.ts | 2 + .../core/__tests__/unit/LexicalEditor.test.ts | 4 +- .../core/__tests__/unit/LexicalNode.test.ts | 2 +- .../lexical/core/__tests__/utils/index.ts | 22 + .../__tests__/unit/LexicalElementNode.test.ts | 13 +- .../unit/LexicalHeadlessEditor.test.ts | 8 +- .../__tests__/unit/LexicalSelection.test.ts | 31 +- .../selection/__tests__/utils/index.ts | 5 +- .../__tests__/unit/LexicalTableNode.test.ts | 2 +- 11 files changed, 2054 insertions(+), 1260 deletions(-) diff --git a/package-lock.json b/package-lock.json index 079e39770..fb6a1d4fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,47 +5,47 @@ "packages": { "": { "dependencies": { - "@codemirror/commands": "^6.8.0", + "@codemirror/commands": "^6.8.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-javascript": "^6.2.3", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-markdown": "^6.3.2", - "@codemirror/lang-php": "^6.0.1", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.3.4", + "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/language": "^6.10.8", - "@codemirror/legacy-modes": "^6.4.3", + "@codemirror/language": "^6.11.3", + "@codemirror/legacy-modes": "^6.5.1", "@codemirror/state": "^6.5.2", - "@codemirror/theme-one-dark": "^6.1.2", - "@codemirror/view": "^6.36.3", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.1", "@lezer/highlight": "^1.2.1", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "@types/jest": "^29.5.14", - "codemirror": "^6.0.1", - "idb-keyval": "^6.2.1", + "@types/jest": "^30.0.0", + "codemirror": "^6.0.2", + "idb-keyval": "^6.2.2", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "snabbdom": "^3.6.2", "sortablejs": "^1.15.6" }, "devDependencies": { - "@eslint/js": "^9.21.0", - "@lezer/generator": "^1.7.2", + "@eslint/js": "^9.34.0", + "@lezer/generator": "^1.8.0", "@types/markdown-it": "^14.1.2", "@types/sortablejs": "^1.15.8", "chokidar-cli": "^3.0", - "esbuild": "^0.25.0", - "eslint": "^9.21.0", - "eslint-plugin-import": "^2.31.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "livereload": "^0.9.3", + "esbuild": "^0.25.9", + "eslint": "^9.34.0", + "eslint-plugin-import": "^2.32.0", + "jest": "^30.1.1", + "jest-environment-jsdom": "^30.1.1", + "livereload": "^0.10.3", "npm-run-all": "^4.1.5", - "sass": "^1.85.0", - "ts-jest": "^29.2.6", + "sass": "^1.91.0", + "ts-jest": "^29.4.1", "ts-node": "^10.9.2", - "typescript": "5.7.*" + "typescript": "5.9.*" } }, "node_modules/@ampproject/remapping": { @@ -62,6 +62,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -536,9 +557,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -636,9 +657,9 @@ } }, "node_modules/@codemirror/lang-markdown": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz", - "integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz", + "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", @@ -678,9 +699,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -779,10 +800,159 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -797,9 +967,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -814,9 +984,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -831,9 +1001,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -848,9 +1018,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -865,9 +1035,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -882,9 +1052,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -899,9 +1069,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -916,9 +1086,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -933,9 +1103,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -950,9 +1120,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -967,9 +1137,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -984,9 +1154,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -1001,9 +1171,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -1018,9 +1188,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -1035,9 +1205,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -1052,9 +1222,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -1069,9 +1239,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -1086,9 +1256,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -1103,9 +1273,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -1120,9 +1290,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -1137,9 +1307,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -1154,9 +1324,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -1171,9 +1341,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -1188,9 +1358,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -1205,9 +1375,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -1279,9 +1449,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1289,9 +1459,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1326,9 +1496,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -1349,13 +1519,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -1428,6 +1598,80 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1546,61 +1790,61 @@ } }, "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.1.tgz", + "integrity": "sha512-f7TGqR1k4GtN5pyFrKmq+ZVndesiwLU33yDpJIGMS9aW+j6hKjue7ljeAdznBsH9kAnxUWe2Y+Y3fLV/FJt3gA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.1.tgz", + "integrity": "sha512-3ncU9peZ3D2VdgRkdZtUceTrDgX5yiDRwAFjtxNfU22IiZrpVWlv/FogzDLYSJQptQGfFo3PcHK86a2oG6WUGg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.1.1", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.5", + "jest-config": "30.1.1", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.0", + "jest-resolve-dependencies": "30.1.1", + "jest-runner": "30.1.1", + "jest-runtime": "30.1.1", + "jest-snapshot": "30.1.1", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "jest-watcher": "30.1.1", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1611,116 +1855,174 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.1.tgz", + "integrity": "sha512-yWHbU+3j7ehQE+NRpnxRvHvpUhoohIjMePBbIr8lfe0cWVb0WeTf80DNux1GPJa18CDHiIU5DtksGUfxcDE+Rw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/fake-timers": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "^29.7.0" + "jest-mock": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.1.1.tgz", + "integrity": "sha512-d7pP9SeIOI6qnrNIS/ds1hlS9jpqh8EywHK0dALSLODZKo2QEGnDNvnPvhRKI0FHWDnE2EMl8CDTP0jM9lhlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.1", + "@jest/fake-timers": "30.1.1", + "@jest/types": "30.0.5", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.1.tgz", + "integrity": "sha512-3vHIHsF+qd3D8FU2c7U5l3rg1fhDwAYcGyHyZAi94YIlTwcJ+boNhRyJf373cl4wxbOX+0Q7dF40RTrTFTSuig==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "expect": "30.1.1", + "jest-snapshot": "30.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.1.tgz", + "integrity": "sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==", "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3" + "@jest/get-type": "30.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.1.tgz", + "integrity": "sha512-fK/25dNgBNYPw3eLi2CRs57g1H04qBAFNMsUY3IRzkfx/m4THe0E1zF+yGQBOMKKc2XQVdc9EYbJ4hEm7/2UtA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.1.tgz", + "integrity": "sha512-NNUUkHT2TU/xztZl6r1UXvJL+zvCwmZsQDmK69fVHHcB9fBtlu3FInnzOve/ZoyKnWY8JXWJNT+Lkmu1+ubXUA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@jest/environment": "30.1.1", + "@jest/expect": "30.1.1", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.1.tgz", + "integrity": "sha512-Hb2Bq80kahOC6Sv2waEaH1rEU6VdFcM6WHaRBWQF9tf30+nJHxhl/Upbgo9+25f0mOgbphxvbwSMjSgy9gW/FA==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", + "@jest/console": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", + "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", + "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -1732,106 +2034,123 @@ } }, "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.1.tgz", + "integrity": "sha512-TkVBc9wuN22TT8hESRFmjjg/xIMu7z0J3UDYtIRydzCqlLPTB7jK1DDBKdnTUZ4zL3z3rnPpzV6rL1Uzh87sXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.1.tgz", + "integrity": "sha512-bMdj7fNu8iZuBPSnbVir5ezvWmVo4jrw7xDE+A33Yb3ENCoiJK9XgOLgal+rJ9XSKjsL7aPUMIo87zhN7I5o2w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@jest/console": "30.1.1", + "@jest/types": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.1.tgz", + "integrity": "sha512-yruRdLXSA3HYD/MTNykgJ6VYEacNcXDFRMqKVAwlYegmxICUiT/B++CNuhJnYJzKYks61iYnjVsMwbUqmmAYJg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", + "@jest/test-result": "30.1.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.1.tgz", + "integrity": "sha512-PHIA2AbAASBfk6evkNifvmx9lkOSkmvaQoO6VSpuL8+kQqDMHeDoJ7RU3YP1wWAMD7AyQn9UL5iheuFYCC4lqQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "write-file-atomic": "^5.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1993,6 +2312,19 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -2303,6 +2635,30 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2311,9 +2667,9 @@ "license": "MIT" }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -2327,13 +2683,13 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" } }, "node_modules/@ssddanbrown/codemirror-lang-smarty": { @@ -2353,16 +2709,6 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -2391,6 +2737,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2427,13 +2784,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/estree": { @@ -2443,16 +2800,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2478,19 +2825,19 @@ } }, "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "expect": "^30.0.0", + "pretty-format": "^30.0.0" } }, "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, "license": "MIT", "dependencies": { @@ -2582,13 +2929,281 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "license": "BSD-3-Clause" + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/acorn": { "version": "8.15.0", @@ -2603,17 +3218,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -2638,16 +3242,13 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -2684,13 +3285,16 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -2857,13 +3461,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2874,13 +3471,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2898,75 +3488,57 @@ } }, "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.1.tgz", + "integrity": "sha512-1bZfC/V03qBCzASvZpNFhx3Ouj6LgOd4KFJm4br/fYOS+tSSvVCE61QmcAVbMTwq/GoB7KN4pzGMoyr9cMxSvQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", + "@jest/transform": "30.1.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.8.0" + "@babel/core": "^7.11.0" } }, "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", "test-exclude": "^6.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/babel-preset-current-node-syntax": { @@ -2997,20 +3569,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.11.0" } }, "node_modules/balanced-match": { @@ -3281,9 +3853,9 @@ } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "funding": [ { "type": "github", @@ -3296,9 +3868,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", "dev": true, "license": "MIT" }, @@ -3388,19 +3960,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3415,28 +3974,6 @@ "dev": true, "license": "MIT" }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3465,46 +4002,32 @@ "node": ">= 8" } }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true, - "license": "MIT" - }, "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", "dependencies": { - "cssom": "~0.3.6" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true, - "license": "MIT" - }, "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -3664,16 +4187,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -3708,15 +4221,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3730,20 +4234,6 @@ "node": ">=0.10.0" } }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3759,21 +4249,12 @@ "node": ">= 0.4" } }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.190", @@ -3974,9 +4455,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3987,32 +4468,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -4038,43 +4519,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4360,29 +4819,31 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.1.tgz", + "integrity": "sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" + "@jest/expect-utils": "30.1.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.1", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/fast-deep-equal": { @@ -4429,39 +4890,6 @@ "node": ">=16.0.0" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4528,21 +4956,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">= 6" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/fs.realpath": { @@ -4709,22 +5150,21 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4743,6 +5183,32 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4792,6 +5258,28 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4893,16 +5381,16 @@ "license": "ISC" }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^2.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-escaper": { @@ -4913,32 +5401,31 @@ "license": "MIT" }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -5588,24 +6075,24 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "istanbul-lib-coverage": "^3.0.0" }, "engines": { "node": ">=10" } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5616,42 +6103,39 @@ "node": ">=8" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "license": "Apache-2.0", + "license": "BlueOak-1.0.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "@isaacs/cliui": "^8.0.2" }, - "bin": { - "jake": "bin/cli.js" + "funding": { + "url": "https://github.com/sponsors/isaacs" }, - "engines": { - "node": ">=10" + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.1.tgz", + "integrity": "sha512-yC3JvpP/ZcAZX5rYCtXO/g9k6VTCQz0VFE2v1FpxytWzUqfDtu0XL/pwnNvptzYItvGwomh1ehomRNMOyhCJKw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" + "@jest/core": "30.1.1", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.1.1" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -5663,76 +6147,75 @@ } }, "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", + "execa": "^5.1.1", + "jest-util": "30.0.5", "p-limit": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.1.tgz", + "integrity": "sha512-M3Vd4x5wD7eSJspuTvRF55AkOOBndRxgW3gqQBDlFvbH3X+ASdi8jc+EqXEeAFd/UHulVYIlC4XKJABOhLw6UA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.1.1", + "@jest/expect": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.1", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.1", + "jest-snapshot": "30.1.1", + "jest-util": "30.0.5", "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.1.tgz", + "integrity": "sha512-xm9llxuh5OoI5KZaYzlMhklryHBwg9LZy/gEaaMlXlxb+cZekGNzukU0iblbDo3XOBuN6N0CgK4ykgNRYSEb6g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" + "@jest/core": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.1.1", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "yargs": "^17.7.2" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -5743,6 +6226,16 @@ } } }, + "node_modules/jest-cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/jest-cli/node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -5790,6 +6283,19 @@ "node": ">=8" } }, + "node_modules/jest-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-cli/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5838,117 +6344,120 @@ } }, "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.1.tgz", + "integrity": "sha512-xuPGUGDw+9fPPnGmddnLnHS/mhKUiJOW7K65vErYmglEPKq65NKwSRchkQ7iv6gqjs2l+YNEsAtbsplxozdOWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.1.1", + "@jest/types": "30.0.5", + "babel-jest": "30.1.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.1.1", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.1", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.0", + "jest-runner": "30.1.1", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", + "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { "@types/node": "*", + "esbuild-register": ">=3.4.0", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "esbuild-register": { + "optional": true + }, "ts-node": { "optional": true } } }, "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.1.tgz", + "integrity": "sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==", "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", "dev": true, "license": "MIT", "dependencies": { - "detect-newline": "^3.0.0" + "detect-newline": "^3.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.1.1.tgz", + "integrity": "sha512-fInyXsHSuPaERmRiub4V6jl6KERXowGqY8AISJrXZjOq7vdP46qecm+GnTngjcUPeHFqrxp1PfP0XuFfKTzA2A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", + "@jest/environment": "30.1.1", + "@jest/environment-jsdom-abstract": "30.1.1", + "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" + "jsdom": "^26.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -5957,120 +6466,110 @@ } }, "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.1.tgz", + "integrity": "sha512-IaMoaA6saxnJimqCppUDqKck+LKM0Jg+OxyMUIvs1yGd2neiC22o8zXo90k04+tO+49OmgMR4jTgM5e4B0S62Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.1.1", + "@jest/fake-timers": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", + "@jest/types": "30.0.5", "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "micromatch": "^4.0.8", "walker": "^1.0.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "fsevents": "^2.3.3" } }, "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", "dev": true, "license": "MIT", "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz", + "integrity": "sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==", "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.1", + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-util": "^29.7.0" + "jest-util": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-pnp-resolver": { @@ -6092,147 +6591,147 @@ } }, "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.0.tgz", + "integrity": "sha512-hASe7D/wRtZw8Cm607NrlF7fi3HWC5wmA5jCVc2QjQAB2pTwP9eVZILGEi6OeSLNUtE1zb04sXRowsdh5CUjwA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.1.tgz", + "integrity": "sha512-tRtaaoH8Ws1Gn1o/9pedt19dvVgr81WwdmvJSP9Ow3amOUOP2nN9j94u5jC9XlIfa2Q1FQKIWWQwL4ajqsjCGQ==", "dev": true, "license": "MIT", "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.1.tgz", + "integrity": "sha512-ATe6372SOfJvCRExtCAr06I4rGujwFdKg44b6i7/aOgFnULwjxzugJ0Y4AnG+jeSeQi8dU7R6oqLGmsxRUbErQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "30.1.1", + "@jest/environment": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.1", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.0", + "jest-runtime": "30.1.1", + "jest-util": "30.0.5", + "jest-watcher": "30.1.1", + "jest-worker": "30.1.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.1.tgz", + "integrity": "sha512-7sOyR0Oekw4OesQqqBHuYJRB52QtXiq0NNgLRzVogiMSxKCMiliUd6RrXHCnG5f12Age/ggidCBiQftzcA9XKw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/environment": "30.1.1", + "@jest/fake-timers": "30.1.1", + "@jest/globals": "30.1.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.0", + "jest-snapshot": "30.1.1", + "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.1.tgz", + "integrity": "sha512-7/iBEzoJqEt2TjkQY+mPLHP8cbPhLReZVkkxjTMzIzoTC4cZufg7HzKo/n9cIkXKj2LG0x3mmBHsZto+7TOmFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.1.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.1.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.1.1", + "jest-matcher-utils": "30.1.1", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot/node_modules/semver": { @@ -6249,38 +6748,50 @@ } }, "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", + "@jest/types": "30.0.5", "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "pretty-format": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-validate/node_modules/camelcase": { @@ -6297,39 +6808,40 @@ } }, "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.1.tgz", + "integrity": "sha512-CrAQ73LlaS6KGQQw6NBi71g7qvP7scy+4+2c0jKX6+CWaYg85lZiig5nQQVTsS5a5sffNPL3uxXnaE9d7v9eQg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/test-result": "30.1.1", + "@jest/types": "30.0.5", "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" + "jest-util": "30.0.5", + "string-length": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-util": "^29.7.0", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { @@ -6368,44 +6880,38 @@ } }, "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -6484,16 +6990,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6535,16 +7031,16 @@ } }, "node_modules/livereload": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", - "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.10.3.tgz", + "integrity": "sha512-llSb8HrtSH7ByPFMc8WTTeW3oy++smwgSA8JVGzEn8KiDPESq6jt1M4ZKKkhKTrhn2wvUOadQq4ip10E5daZ3w==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^3.5.0", - "livereload-js": "^3.3.1", - "opts": ">= 1.2.0", - "ws": "^7.4.3" + "chokidar": "^4.0.3", + "livereload-js": "^4.0.2", + "opts": "^2.0.2", + "ws": "^8.4.3" }, "bin": { "livereload": "bin/livereload.js" @@ -6554,32 +7050,40 @@ } }, "node_modules/livereload-js": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz", - "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-4.0.2.tgz", + "integrity": "sha512-Fy7VwgQNiOkynYyNBTo3v9hQUhcW5pFAheJN148+DTgpShjsy/22pLHKKwDK5v0kOsZsJBK+6q1PMgLvRmrwFQ==", "dev": true, "license": "MIT" }, - "node_modules/livereload/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "node_modules/livereload/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/livereload/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.3.0" + "node": ">= 14.18.0" }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/load-json-file": { @@ -6790,29 +7294,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6846,6 +7327,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6853,6 +7344,22 @@ "dev": true, "license": "MIT" }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6860,6 +7367,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -7113,9 +7627,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.20", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", - "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", "dev": true, "license": "MIT" }, @@ -7327,6 +7841,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7422,6 +7943,30 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -7576,17 +8121,17 @@ } }, "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -7601,33 +8146,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7648,9 +8166,9 @@ } }, "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", "dev": true, "funding": [ { @@ -7664,13 +8182,6 @@ ], "license": "MIT" }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7766,13 +8277,6 @@ "dev": true, "license": "ISC" }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -7827,15 +8331,12 @@ "node": ">=4" } }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } + "license": "MIT" }, "node_modules/safe-array-concat": { "version": "1.1.3", @@ -7900,9 +8401,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.89.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", - "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "dev": true, "license": "MIT", "dependencies": { @@ -8148,13 +8649,6 @@ "dev": true, "license": "ISC" }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8302,6 +8796,29 @@ "node": ">=10" } }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -8317,6 +8834,62 @@ "node": ">=6" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -8419,6 +8992,23 @@ } }, "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -8431,6 +9021,16 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -8502,6 +9102,22 @@ "dev": true, "license": "MIT" }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -8517,6 +9133,48 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8537,44 +9195,41 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" + "node": ">=16" } }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/ts-jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz", - "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==", + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", - "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", @@ -8724,6 +9379,14 @@ "node": ">=4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8839,9 +9502,9 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8858,6 +9521,20 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -8883,14 +9560,39 @@ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "node_modules/update-browserslist-db": { @@ -8934,17 +9636,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8985,16 +9676,16 @@ "license": "MIT" }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/walker": { @@ -9018,40 +9709,40 @@ } }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/which": { @@ -9176,6 +9867,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", @@ -9191,6 +9889,80 @@ "node": ">=6" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", @@ -9252,17 +10024,30 @@ "license": "ISC" }, "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "signal-exit": "^4.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ws": { @@ -9288,13 +10073,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { diff --git a/package.json b/package.json index 151338d8c..9ec94acc2 100644 --- a/package.json +++ b/package.json @@ -19,43 +19,43 @@ "test": "jest" }, "devDependencies": { - "@eslint/js": "^9.21.0", - "@lezer/generator": "^1.7.2", + "@eslint/js": "^9.34.0", + "@lezer/generator": "^1.8.0", "@types/markdown-it": "^14.1.2", "@types/sortablejs": "^1.15.8", "chokidar-cli": "^3.0", - "esbuild": "^0.25.0", - "eslint": "^9.21.0", - "eslint-plugin-import": "^2.31.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "livereload": "^0.9.3", + "esbuild": "^0.25.9", + "eslint": "^9.34.0", + "eslint-plugin-import": "^2.32.0", + "jest": "^30.1.1", + "jest-environment-jsdom": "^30.1.1", + "livereload": "^0.10.3", "npm-run-all": "^4.1.5", - "sass": "^1.85.0", - "ts-jest": "^29.2.6", + "sass": "^1.91.0", + "ts-jest": "^29.4.1", "ts-node": "^10.9.2", - "typescript": "5.7.*" + "typescript": "5.9.*" }, "dependencies": { - "@codemirror/commands": "^6.8.0", + "@codemirror/commands": "^6.8.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-javascript": "^6.2.3", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-markdown": "^6.3.2", - "@codemirror/lang-php": "^6.0.1", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.3.4", + "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/language": "^6.10.8", - "@codemirror/legacy-modes": "^6.4.3", + "@codemirror/language": "^6.11.3", + "@codemirror/legacy-modes": "^6.5.1", "@codemirror/state": "^6.5.2", - "@codemirror/theme-one-dark": "^6.1.2", - "@codemirror/view": "^6.36.3", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.38.1", "@lezer/highlight": "^1.2.1", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "@types/jest": "^29.5.14", - "codemirror": "^6.0.1", - "idb-keyval": "^6.2.1", + "@types/jest": "^30.0.0", + "codemirror": "^6.0.2", + "idb-keyval": "^6.2.2", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "snabbdom": "^3.6.2", diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index 7051336d5..36e2db547 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -2708,6 +2708,8 @@ export function updateDOMSelection( const range = document.createRange(); range.selectNode(selectionTarget); selectionRect = range.getBoundingClientRect(); + } else if (selectionTarget instanceof Range) { + selectionRect = selectionTarget.getBoundingClientRect(); } else { selectionRect = selectionTarget.getBoundingClientRect(); } diff --git a/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalEditor.test.ts b/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalEditor.test.ts index a54d33ca4..f195974d0 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalEditor.test.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalEditor.test.ts @@ -2510,8 +2510,8 @@ describe('LexicalEditor tests', () => { ); }); - expect(onError).toBeCalledWith(updateError); - expect(textListener).toBeCalledWith('Hello\n\nworld'); + expect(onError).toHaveBeenCalledWith(updateError); + expect(textListener).toHaveBeenCalledWith('Hello\n\nworld'); expect(updateListener.mock.lastCall[0].prevEditorState).toBe(editorState); }); diff --git a/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalNode.test.ts b/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalNode.test.ts index fcf666213..33c59bf5f 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalNode.test.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/unit/LexicalNode.test.ts @@ -138,7 +138,7 @@ describe('LexicalNode tests', () => { const validNode = new TextNode(textNode.__text, textNode.__key); expect(textNode.getLatest()).toBe(textNode); expect(validNode.getLatest()).toBe(textNode); - expect(() => new TestNode(textNode.__key)).toThrowError( + expect(() => new TestNode(textNode.__key)).toThrow( /TestNode.*re-use key.*TextNode/, ); }); diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index fd87877ee..00c5ec796 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -864,4 +864,26 @@ export function dispatchEditorMouseClick(editor: LexicalEditor, clientX: number, }); dom?.dispatchEvent(event); editor.commitUpdates(); +} + +export function patchRange() { + const RangePrototype = Object.getPrototypeOf(document.createRange()); + RangePrototype.getBoundingClientRect = function (): DOMRect { + const rect = { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + }; + return { + ...rect, + toJSON() { + return rect; + }, + }; + }; } \ No newline at end of file diff --git a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalElementNode.test.ts b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalElementNode.test.ts index 6e3a3861a..0bc7646c9 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalElementNode.test.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalElementNode.test.ts @@ -18,10 +18,12 @@ import { } from 'lexical'; import { - $createTestElementNode, - createTestEditor, + $createTestElementNode, + createTestEditor, patchRange, } from '../../../__tests__/utils'; +patchRange(); + describe('LexicalElementNode tests', () => { let container: HTMLElement; @@ -54,6 +56,7 @@ describe('LexicalElementNode tests', () => { editor = createTestEditor(); editor.setRootElement(root); + root.focus(); // Insert initial block await update(() => { @@ -63,11 +66,11 @@ describe('LexicalElementNode tests', () => { // Prevent text nodes from combining. text2.setMode('segmented'); const text3 = $createTextNode('Baz'); - // Some operations require a selection to exist, hence - // we make a selection in the setup code. - text.select(0, 0); block.append(text, text2, text3); $getRoot().append(block); + // Some operations require a selection to exist, hence + // we make a selection in the setup code. + text.select(0, 0); }); } diff --git a/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts b/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts index c03f1bdb2..cee414919 100644 --- a/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts +++ b/resources/js/wysiwyg/lexical/headless/__tests__/unit/LexicalHeadlessEditor.test.ts @@ -146,12 +146,12 @@ describe('LexicalHeadlessEditor', () => { editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, 'foo'); }); - expect(onUpdate).toBeCalled(); - expect(onCommand).toBeCalledWith('foo', expect.anything()); - expect(onTransform).toBeCalledWith( + expect(onUpdate).toHaveBeenCalled(); + expect(onCommand).toHaveBeenCalledWith('foo', expect.anything()); + expect(onTransform).toHaveBeenCalledWith( expect.objectContaining({__type: 'paragraph'}), ); - expect(onTextContent).toBeCalledWith('Helloworld'); + expect(onTextContent).toHaveBeenCalledWith('Helloworld'); }); it('can preserve selection for pending editor state (within update loop)', async () => { diff --git a/resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelection.test.ts b/resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelection.test.ts index cc09d1735..14c8b5121 100644 --- a/resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelection.test.ts +++ b/resources/js/wysiwyg/lexical/selection/__tests__/unit/LexicalSelection.test.ts @@ -37,12 +37,12 @@ import { TextNode, } from 'lexical'; import { - $assertRangeSelection, - $createTestDecoratorNode, - $createTestElementNode, - createTestEditor, - initializeClipboard, - invariant, + $assertRangeSelection, + $createTestDecoratorNode, + $createTestElementNode, + createTestEditor, + initializeClipboard, + invariant, patchRange, } from 'lexical/__tests__/utils'; import { @@ -91,24 +91,7 @@ jest.mock('lexical/shared/environment', () => { return {...originalModule, IS_FIREFOX: true}; }); -Range.prototype.getBoundingClientRect = function (): DOMRect { - const rect = { - bottom: 0, - height: 0, - left: 0, - right: 0, - top: 0, - width: 0, - x: 0, - y: 0, - }; - return { - ...rect, - toJSON() { - return rect; - }, - }; -}; +patchRange(); describe('LexicalSelection tests', () => { let container: HTMLElement; diff --git a/resources/js/wysiwyg/lexical/selection/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/selection/__tests__/utils/index.ts index 84c82edec..39d4a04bd 100644 --- a/resources/js/wysiwyg/lexical/selection/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/selection/__tests__/utils/index.ts @@ -422,9 +422,6 @@ export function setNativeSelection( range.setEnd(focusNode, focusOffset); domSelection.removeAllRanges(); domSelection.addRange(range); - Promise.resolve().then(() => { - document.dispatchEvent(new Event('selectionchange')); - }); } export function setNativeSelectionWithPaths( @@ -647,6 +644,8 @@ export async function applySelectionInputs( editor: LexicalEditor, ) { const rootElement = editor.getRootElement()!; + // Set initial focus as if we're in the editor + rootElement.focus(); for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; diff --git a/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts b/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts index 2879decda..560d316fe 100644 --- a/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts +++ b/resources/js/wysiwyg/lexical/table/__tests__/unit/LexicalTableNode.test.ts @@ -114,7 +114,7 @@ describe('LexicalTableNode tests', () => { }); // Make sure paragraph is inserted inside empty cells expect(testEnv.innerHTML).toBe( - `

Hello there

General Kenobi!

Lexical is nice


`, + `

Hello there

General Kenobi!

Lexical is nice


`, ); }); From 5ea4e1e935624e060ad2b4e257a76539ec1a4746 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 2 Sep 2025 10:20:10 +0100 Subject: [PATCH 018/139] Maintenance: Removed unused comments text column Has been redundant and unused for a about a year now. Closes #4821 --- ..._02_111542_remove_comments_text_column.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 database/migrations/2025_09_02_111542_remove_comments_text_column.php diff --git a/database/migrations/2025_09_02_111542_remove_comments_text_column.php b/database/migrations/2025_09_02_111542_remove_comments_text_column.php new file mode 100644 index 000000000..8caa05e54 --- /dev/null +++ b/database/migrations/2025_09_02_111542_remove_comments_text_column.php @@ -0,0 +1,28 @@ +dropColumn('text'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comments', function (Blueprint $table) { + $table->longText('text')->nullable(); + }); + } +}; From 1e34954554d672446693bcc74bf7f9aae1936802 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 2 Sep 2025 11:10:47 +0100 Subject: [PATCH 019/139] Maintenance: Continued work towards PHPstan level 2 Updated html description code to be behind a proper interface. Set new convention for mode traits/interfaces. --- app/Entities/Models/Book.php | 5 +-- app/Entities/Models/Bookshelf.php | 4 +-- app/Entities/Models/Chapter.php | 4 +-- ...CoverImage.php => CoverImageInterface.php} | 2 +- .../{Deletable.php => DeletableInterface.php} | 2 +- app/Entities/Models/Deletion.php | 2 +- app/Entities/Models/Entity.php | 2 +- app/Entities/Models/HasHtmlDescription.php | 21 ----------- .../Models/HtmlDescriptionInterface.php | 17 +++++++++ app/Entities/Models/HtmlDescriptionTrait.php | 35 +++++++++++++++++++ app/Entities/Queries/PageQueries.php | 3 ++ app/Entities/Repos/BaseRepo.php | 20 +++++------ app/Entities/Tools/Cloner.php | 4 +-- app/Entities/Tools/PageIncludeParser.php | 7 ++-- app/Entities/Tools/TrashCan.php | 4 +-- app/References/ReferenceUpdater.php | 13 ++++--- app/Sorting/SortRuleController.php | 2 +- .../Controllers/ImageGalleryApiController.php | 4 +-- 18 files changed, 94 insertions(+), 57 deletions(-) rename app/Entities/Models/{HasCoverImage.php => CoverImageInterface.php} (92%) rename app/Entities/Models/{Deletable.php => DeletableInterface.php} (90%) delete mode 100644 app/Entities/Models/HasHtmlDescription.php create mode 100644 app/Entities/Models/HtmlDescriptionInterface.php create mode 100644 app/Entities/Models/HtmlDescriptionTrait.php diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index ede4fc7d5..88e3b85ba 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -26,10 +26,10 @@ use Illuminate\Support\Collection; * @property ?Page $defaultTemplate * @property ?SortRule $sortRule */ -class Book extends Entity implements HasCoverImage +class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterface { use HasFactory; - use HasHtmlDescription; + use HtmlDescriptionTrait; public float $searchFactor = 1.2; @@ -111,6 +111,7 @@ class Book extends Entity implements HasCoverImage /** * Get all chapters within this book. + * @return HasMany */ public function chapters(): HasMany { diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index 9ffa0ea9c..5b403d9c0 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -8,10 +8,10 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; -class Bookshelf extends Entity implements HasCoverImage +class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionInterface { use HasFactory; - use HasHtmlDescription; + use HtmlDescriptionTrait; protected $table = 'bookshelves'; diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index 088d199da..03e819c06 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -14,10 +14,10 @@ use Illuminate\Support\Collection; * @property ?int $default_template_id * @property ?Page $defaultTemplate */ -class Chapter extends BookChild +class Chapter extends BookChild implements HtmlDescriptionInterface { use HasFactory; - use HasHtmlDescription; + use HtmlDescriptionTrait; public float $searchFactor = 1.2; diff --git a/app/Entities/Models/HasCoverImage.php b/app/Entities/Models/CoverImageInterface.php similarity index 92% rename from app/Entities/Models/HasCoverImage.php rename to app/Entities/Models/CoverImageInterface.php index f665efce6..5f781fe02 100644 --- a/app/Entities/Models/HasCoverImage.php +++ b/app/Entities/Models/CoverImageInterface.php @@ -4,7 +4,7 @@ namespace BookStack\Entities\Models; use Illuminate\Database\Eloquent\Relations\BelongsTo; -interface HasCoverImage +interface CoverImageInterface { /** * Get the cover image for this item. diff --git a/app/Entities/Models/Deletable.php b/app/Entities/Models/DeletableInterface.php similarity index 90% rename from app/Entities/Models/Deletable.php rename to app/Entities/Models/DeletableInterface.php index a2c7fad81..f771d9c69 100644 --- a/app/Entities/Models/Deletable.php +++ b/app/Entities/Models/DeletableInterface.php @@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; * A model that can be deleted in a manner that deletions * are tracked to be part of the recycle bin system. */ -interface Deletable +interface DeletableInterface { public function deletions(): MorphMany; } diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php index a73437c94..55589f61e 100644 --- a/app/Entities/Models/Deletion.php +++ b/app/Entities/Models/Deletion.php @@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; * @property int $deleted_by * @property string $deletable_type * @property int $deletable_id - * @property Deletable $deletable + * @property DeletableInterface $deletable */ class Deletion extends Model implements Loggable { diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 1ef4e618d..46b29f93b 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -49,7 +49,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static Builder withLastView() * @method static Builder withViewCount() */ -abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, Deletable, Loggable +abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, DeletableInterface, Loggable { use SoftDeletes; use HasCreatorAndUpdater; diff --git a/app/Entities/Models/HasHtmlDescription.php b/app/Entities/Models/HasHtmlDescription.php deleted file mode 100644 index c9f08616d..000000000 --- a/app/Entities/Models/HasHtmlDescription.php +++ /dev/null @@ -1,21 +0,0 @@ -description_html ?: '

' . nl2br(e($this->description)) . '

'; - return HtmlContentFilter::removeScriptsFromHtmlString($html); - } -} diff --git a/app/Entities/Models/HtmlDescriptionInterface.php b/app/Entities/Models/HtmlDescriptionInterface.php new file mode 100644 index 000000000..ffe7f0c5f --- /dev/null +++ b/app/Entities/Models/HtmlDescriptionInterface.php @@ -0,0 +1,17 @@ +description_html ?: '

' . nl2br(e($this->description)) . '

'; + if ($raw) { + return $html; + } + + return HtmlContentFilter::removeScriptsFromHtmlString($html); + } + + public function setDescriptionHtml(string $html, string|null $plaintext = null): void + { + $this->description_html = $html; + + if ($plaintext !== null) { + $this->description = $plaintext; + } + + if (empty($html) && !empty($plaintext)) { + $this->description_html = $this->descriptionHtml(); + } + } +} diff --git a/app/Entities/Queries/PageQueries.php b/app/Entities/Queries/PageQueries.php index 06298f470..f821ee86a 100644 --- a/app/Entities/Queries/PageQueries.php +++ b/app/Entities/Queries/PageQueries.php @@ -66,6 +66,9 @@ class PageQueries implements ProvidesEntityQueries }); } + /** + * @return Builder + */ public function visibleForList(): Builder { return $this->start() diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index ac5a44e67..f4d05d8af 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -7,8 +7,9 @@ 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; -use BookStack\Entities\Models\HasHtmlDescription; +use BookStack\Entities\Models\CoverImageInterface; +use BookStack\Entities\Models\HtmlDescriptionInterface; +use BookStack\Entities\Models\HtmlDescriptionTrait; use BookStack\Entities\Queries\PageQueries; use BookStack\Exceptions\ImageUploadException; use BookStack\References\ReferenceStore; @@ -88,7 +89,7 @@ class BaseRepo /** * Update the given items' cover image, or clear it. * - * @param Entity&HasCoverImage $entity + * @param Entity&CoverImageInterface $entity * * @throws ImageUploadException * @throws \Exception @@ -150,18 +151,17 @@ class BaseRepo protected function updateDescription(Entity $entity, array $input): void { - if (!in_array(HasHtmlDescription::class, class_uses($entity))) { + if (!($entity instanceof HtmlDescriptionInterface)) { return; } - /** @var HasHtmlDescription $entity */ if (isset($input['description_html'])) { - $entity->description_html = HtmlDescriptionFilter::filterFromString($input['description_html']); - $entity->description = html_entity_decode(strip_tags($input['description_html'])); + $entity->setDescriptionHtml( + HtmlDescriptionFilter::filterFromString($input['description_html']), + html_entity_decode(strip_tags($input['description_html'])) + ); } else if (isset($input['description'])) { - $entity->description = $input['description']; - $entity->description_html = ''; - $entity->description_html = $entity->descriptionHtml(); + $entity->setDescriptionHtml('', $input['description']); } } } diff --git a/app/Entities/Tools/Cloner.php b/app/Entities/Tools/Cloner.php index 2be6083e3..87aa770c0 100644 --- a/app/Entities/Tools/Cloner.php +++ b/app/Entities/Tools/Cloner.php @@ -7,7 +7,7 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasCoverImage; +use BookStack\Entities\Models\CoverImageInterface; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Repos\ChapterRepo; @@ -105,7 +105,7 @@ class Cloner $inputData['tags'] = $this->entityTagsToInputArray($entity); // Add a cover to the data if existing on the original entity - if ($entity instanceof HasCoverImage) { + if ($entity instanceof CoverImageInterface) { $cover = $entity->cover()->first(); if ($cover) { $inputData['image'] = $this->imageToUploadedFile($cover); diff --git a/app/Entities/Tools/PageIncludeParser.php b/app/Entities/Tools/PageIncludeParser.php index e0b89f158..329a7633f 100644 --- a/app/Entities/Tools/PageIncludeParser.php +++ b/app/Entities/Tools/PageIncludeParser.php @@ -7,7 +7,6 @@ use Closure; use DOMDocument; use DOMElement; use DOMNode; -use DOMText; class PageIncludeParser { @@ -159,7 +158,7 @@ class PageIncludeParser /** * Splits the given $parentNode at the location of the $domNode within it. - * Attempts replicate the original $parentNode, moving some of their parent + * Attempts to replicate the original $parentNode, moving some of their parent * children in where needed, before adding the $domNode between. */ protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode): void @@ -171,6 +170,10 @@ class PageIncludeParser } $parentClone = $parentNode->cloneNode(); + if (!($parentClone instanceof DOMElement)) { + return; + } + $parentNode->parentNode->insertBefore($parentClone, $parentNode); $parentClone->removeAttribute('id'); diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 5e8a93719..d457d4f48 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -8,7 +8,7 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasCoverImage; +use BookStack\Entities\Models\CoverImageInterface; use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\EntityQueries; use BookStack\Exceptions\NotifyException; @@ -398,7 +398,7 @@ class TrashCan $entity->referencesTo()->delete(); $entity->referencesFrom()->delete(); - if ($entity instanceof HasCoverImage && $entity->cover()->exists()) { + if ($entity instanceof CoverImageInterface && $entity->cover()->exists()) { $imageService = app()->make(ImageService::class); $imageService->destroy($entity->cover()->first()); } diff --git a/app/References/ReferenceUpdater.php b/app/References/ReferenceUpdater.php index db355f211..5f1d711e9 100644 --- a/app/References/ReferenceUpdater.php +++ b/app/References/ReferenceUpdater.php @@ -4,7 +4,8 @@ namespace BookStack\References; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Entity; -use BookStack\Entities\Models\HasHtmlDescription; +use BookStack\Entities\Models\HtmlDescriptionInterface; +use BookStack\Entities\Models\HtmlDescriptionTrait; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\RevisionRepo; use BookStack\Util\HtmlDocument; @@ -61,20 +62,18 @@ class ReferenceUpdater { if ($entity instanceof Page) { $this->updateReferencesWithinPage($entity, $oldLink, $newLink); - return; } - if (in_array(HasHtmlDescription::class, class_uses($entity))) { + if ($entity instanceof HtmlDescriptionInterface) { $this->updateReferencesWithinDescription($entity, $oldLink, $newLink); } } - protected function updateReferencesWithinDescription(Entity $entity, string $oldLink, string $newLink): void + protected function updateReferencesWithinDescription(Entity&HtmlDescriptionInterface $entity, string $oldLink, string $newLink): void { - /** @var HasHtmlDescription&Entity $entity */ $entity = (clone $entity)->refresh(); - $html = $this->updateLinksInHtml($entity->description_html ?: '', $oldLink, $newLink); - $entity->description_html = $html; + $html = $this->updateLinksInHtml($entity->descriptionHtml(true) ?: '', $oldLink, $newLink); + $entity->setDescriptionHtml($html); $entity->save(); } diff --git a/app/Sorting/SortRuleController.php b/app/Sorting/SortRuleController.php index 96b8e8ef5..a124ffa9c 100644 --- a/app/Sorting/SortRuleController.php +++ b/app/Sorting/SortRuleController.php @@ -29,7 +29,7 @@ class SortRuleController extends Controller $operations = SortRuleOperation::fromSequence($request->input('sequence')); if (count($operations) === 0) { - return redirect()->withInput()->withErrors(['sequence' => 'No operations set.']); + return redirect('/settings/sorting/rules/new')->withInput()->withErrors(['sequence' => 'No operations set.']); } $rule = new SortRule(); diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index 6d4657a7a..d2a750c45 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -146,10 +146,10 @@ class ImageGalleryApiController extends ApiController $data['content']['html'] = "
id}\">
"; $data['content']['markdown'] = $data['content']['html']; } else { - $escapedDisplayThumb = htmlentities($image->thumbs['display']); + $escapedDisplayThumb = htmlentities($image->getAttribute('thumbs')['display']); $data['content']['html'] = "\"{$escapedName}\""; $mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name)); - $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display'])); + $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->getAttribute('thumbs')['display'])); $data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})"; } From cee23de6c5350101e16e1aead59e0219e9e2771e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 2 Sep 2025 16:02:52 +0100 Subject: [PATCH 020/139] Maintenance: Reached PHPstan level 2 Reworked some stuff around slugs to use interface in a better way. Also standardised phpdoc to use @return instead of @returns --- app/Access/Saml2Service.php | 2 +- app/Access/SocialDriverManager.php | 2 +- app/Activity/Models/Comment.php | 7 +++-- app/Activity/WatchLevels.php | 4 +-- app/App/Model.php | 2 +- .../{Sluggable.php => SluggableInterface.php} | 5 +-- .../Controllers/RecycleBinApiController.php | 19 ++++++++---- app/Entities/Models/Book.php | 3 +- app/Entities/Models/Bookshelf.php | 1 + app/Entities/Models/Chapter.php | 4 +-- app/Entities/Models/Entity.php | 31 ++++++++++++++++--- app/Entities/Queries/BookQueries.php | 3 ++ app/Entities/Queries/BookshelfQueries.php | 3 ++ app/Entities/Queries/EntityQueries.php | 2 +- .../Queries/ProvidesEntityQueries.php | 3 +- app/Entities/Repos/BaseRepo.php | 6 ++-- app/Entities/Tools/SlugGenerator.php | 12 +++---- app/Permissions/PermissionApplicator.php | 15 ++++----- app/References/CrossLinkParser.php | 2 +- app/Search/SearchIndex.php | 12 +++---- app/Sorting/BookSorter.php | 2 +- app/Theming/ThemeEvents.php | 12 +++---- app/Uploads/Attachment.php | 3 +- app/Uploads/Image.php | 3 +- app/Uploads/ImageService.php | 2 +- app/Uploads/ImageStorageDisk.php | 2 +- app/Users/Controllers/UserApiController.php | 2 +- app/Users/Models/HasCreatorAndUpdater.php | 5 +++ app/Users/Models/HasOwner.php | 19 ------------ app/Users/Models/OwnableInterface.php | 8 +++++ app/Users/Models/Role.php | 1 + app/Users/Models/User.php | 6 ++-- phpcs.xml | 2 +- phpstan.neon.dist | 2 +- 34 files changed, 118 insertions(+), 89 deletions(-) rename app/App/{Sluggable.php => SluggableInterface.php} (75%) delete mode 100644 app/Users/Models/HasOwner.php create mode 100644 app/Users/Models/OwnableInterface.php diff --git a/app/Access/Saml2Service.php b/app/Access/Saml2Service.php index bb7e9b572..106a7a229 100644 --- a/app/Access/Saml2Service.php +++ b/app/Access/Saml2Service.php @@ -51,7 +51,7 @@ class Saml2Service * Returns the SAML2 request ID, and the URL to redirect the user to. * * @throws Error - * @returns array{url: string, id: ?string} + * @return array{url: string, id: ?string} */ public function logout(User $user): array { diff --git a/app/Access/SocialDriverManager.php b/app/Access/SocialDriverManager.php index dafc0e82d..efafab560 100644 --- a/app/Access/SocialDriverManager.php +++ b/app/Access/SocialDriverManager.php @@ -55,7 +55,7 @@ class SocialDriverManager /** * Gets the names of the active social drivers, keyed by driver id. - * @returns array + * @return array */ public function getActive(): array { diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index 91cea4fe0..6b05a9bab 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -4,6 +4,8 @@ namespace BookStack\Activity\Models; use BookStack\App\Model; use BookStack\Users\Models\HasCreatorAndUpdater; +use BookStack\Users\Models\OwnableInterface; +use BookStack\Users\Models\User; use BookStack\Util\HtmlContentFilter; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -17,12 +19,10 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; * @property int $local_id * @property string $entity_type * @property int $entity_id - * @property int $created_by - * @property int $updated_by * @property string $content_ref * @property bool $archived */ -class Comment extends Model implements Loggable +class Comment extends Model implements Loggable, OwnableInterface { use HasFactory; use HasCreatorAndUpdater; @@ -39,6 +39,7 @@ class Comment extends Model implements Loggable /** * Get the parent comment this is in reply to (if existing). + * @return BelongsTo */ public function parent(): BelongsTo { diff --git a/app/Activity/WatchLevels.php b/app/Activity/WatchLevels.php index de3c5e122..edbece2d3 100644 --- a/app/Activity/WatchLevels.php +++ b/app/Activity/WatchLevels.php @@ -36,7 +36,7 @@ class WatchLevels /** * Get all the possible values as an option_name => value array. - * @returns array + * @return array */ public static function all(): array { @@ -50,7 +50,7 @@ class WatchLevels /** * Get the watch options suited for the given entity. - * @returns array + * @return array */ public static function allSuitedFor(Entity $entity): array { diff --git a/app/App/Model.php b/app/App/Model.php index 8de5a2762..e1c7511c1 100644 --- a/app/App/Model.php +++ b/app/App/Model.php @@ -8,7 +8,7 @@ class Model extends EloquentModel { /** * Provides public access to get the raw attribute value from the model. - * Used in areas where no mutations are required but performance is critical. + * Used in areas where no mutations are required, but performance is critical. * * @return mixed */ diff --git a/app/App/Sluggable.php b/app/App/SluggableInterface.php similarity index 75% rename from app/App/Sluggable.php rename to app/App/SluggableInterface.php index f8da2e265..96af49cd3 100644 --- a/app/App/Sluggable.php +++ b/app/App/SluggableInterface.php @@ -5,11 +5,8 @@ namespace BookStack\App; /** * Assigned to models that can have slugs. * Must have the below properties. - * - * @property int $id - * @property string $name */ -interface Sluggable +interface SluggableInterface { /** * Regenerate the slug for this model. diff --git a/app/Entities/Controllers/RecycleBinApiController.php b/app/Entities/Controllers/RecycleBinApiController.php index bf22d7dcd..fdc24ddf8 100644 --- a/app/Entities/Controllers/RecycleBinApiController.php +++ b/app/Entities/Controllers/RecycleBinApiController.php @@ -6,10 +6,10 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Deletion; +use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\DeletionRepo; use BookStack\Http\ApiController; -use Closure; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\HasMany; class RecycleBinApiController extends ApiController { @@ -40,7 +40,7 @@ class RecycleBinApiController extends ApiController 'updated_at', 'deletable_type', 'deletable_id', - ], [Closure::fromCallable([$this, 'listFormatter'])]); + ], [$this->listFormatter(...)]); } /** @@ -72,7 +72,6 @@ class RecycleBinApiController extends ApiController protected function listFormatter(Deletion $deletion) { $deletable = $deletion->deletable; - $withTrashedQuery = fn (Builder $query) => $query->withTrashed(); if ($deletable instanceof BookChild) { $parent = $deletable->getParent(); @@ -81,11 +80,19 @@ class RecycleBinApiController extends ApiController } if ($deletable instanceof Book || $deletable instanceof Chapter) { - $countsToLoad = ['pages' => $withTrashedQuery]; + $countsToLoad = ['pages' => static::withTrashedQuery(...)]; if ($deletable instanceof Book) { - $countsToLoad['chapters'] = $withTrashedQuery; + $countsToLoad['chapters'] = static::withTrashedQuery(...); } $deletable->loadCount($countsToLoad); } } + + /** + * @param HasMany $query + */ + protected static function withTrashedQuery(HasMany $query): void + { + $query->withTrashed(); + } } diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 88e3b85ba..5f54e0f6a 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -95,6 +95,7 @@ class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterfa /** * Get all pages within this book. + * @return HasMany */ public function pages(): HasMany { @@ -111,7 +112,7 @@ class Book extends Entity implements CoverImageInterface, HtmlDescriptionInterfa /** * Get all chapters within this book. - * @return HasMany + * @return HasMany */ public function chapters(): HasMany { diff --git a/app/Entities/Models/Bookshelf.php b/app/Entities/Models/Bookshelf.php index 5b403d9c0..9ae52abcb 100644 --- a/app/Entities/Models/Bookshelf.php +++ b/app/Entities/Models/Bookshelf.php @@ -70,6 +70,7 @@ class Bookshelf extends Entity implements CoverImageInterface, HtmlDescriptionIn /** * Get the cover image of the shelf. + * @return BelongsTo */ public function cover(): BelongsTo { diff --git a/app/Entities/Models/Chapter.php b/app/Entities/Models/Chapter.php index 03e819c06..d70a49e7a 100644 --- a/app/Entities/Models/Chapter.php +++ b/app/Entities/Models/Chapter.php @@ -27,7 +27,7 @@ class Chapter extends BookChild implements HtmlDescriptionInterface /** * Get the pages that this chapter contains. * - * @return HasMany + * @return HasMany */ public function pages(string $dir = 'ASC'): HasMany { @@ -60,7 +60,7 @@ class Chapter extends BookChild implements HtmlDescriptionInterface /** * Get the visible pages in this chapter. - * @returns Collection + * @return Collection */ public function getVisiblePages(): Collection { diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 46b29f93b..31511aa83 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -12,7 +12,7 @@ use BookStack\Activity\Models\View; use BookStack\Activity\Models\Viewable; use BookStack\Activity\Models\Watch; use BookStack\App\Model; -use BookStack\App\Sluggable; +use BookStack\App\SluggableInterface; use BookStack\Entities\Tools\SlugGenerator; use BookStack\Permissions\JointPermissionBuilder; use BookStack\Permissions\Models\EntityPermission; @@ -22,7 +22,8 @@ use BookStack\References\Reference; use BookStack\Search\SearchIndex; use BookStack\Search\SearchTerm; use BookStack\Users\Models\HasCreatorAndUpdater; -use BookStack\Users\Models\HasOwner; +use BookStack\Users\Models\OwnableInterface; +use BookStack\Users\Models\User; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; @@ -43,17 +44,23 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property Carbon $deleted_at * @property int $created_by * @property int $updated_by + * @property int $owned_by * @property Collection $tags * * @method static Entity|Builder visible() * @method static Builder withLastView() * @method static Builder withViewCount() */ -abstract class Entity extends Model implements Sluggable, Favouritable, Viewable, DeletableInterface, Loggable +abstract class Entity extends Model implements + SluggableInterface, + Favouritable, + Viewable, + DeletableInterface, + OwnableInterface, + Loggable { use SoftDeletes; use HasCreatorAndUpdater; - use HasOwner; /** * @var string - Name of property where the main text content is found @@ -200,6 +207,20 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable return $this->morphMany(JointPermission::class, 'entity'); } + /** + * Get the user who owns this entity. + * @return BelongsTo + */ + public function ownedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'owned_by'); + } + + public function getOwnerFieldName(): string + { + return 'owned_by'; + } + /** * Get the related delete records for this entity. */ @@ -318,7 +339,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable */ public function refreshSlug(): string { - $this->slug = app()->make(SlugGenerator::class)->generate($this); + $this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name); return $this->slug; } diff --git a/app/Entities/Queries/BookQueries.php b/app/Entities/Queries/BookQueries.php index 534640621..5ff22252c 100644 --- a/app/Entities/Queries/BookQueries.php +++ b/app/Entities/Queries/BookQueries.php @@ -13,6 +13,9 @@ class BookQueries implements ProvidesEntityQueries 'created_at', 'updated_at', 'image_id', 'owned_by', ]; + /** + * @return Builder + */ public function start(): Builder { return Book::query(); diff --git a/app/Entities/Queries/BookshelfQueries.php b/app/Entities/Queries/BookshelfQueries.php index 19717fb7c..874bc7f2f 100644 --- a/app/Entities/Queries/BookshelfQueries.php +++ b/app/Entities/Queries/BookshelfQueries.php @@ -13,6 +13,9 @@ class BookshelfQueries implements ProvidesEntityQueries 'created_at', 'updated_at', 'image_id', 'owned_by', ]; + /** + * @return Builder + */ public function start(): Builder { return Bookshelf::query(); diff --git a/app/Entities/Queries/EntityQueries.php b/app/Entities/Queries/EntityQueries.php index 36dc6c0bc..0d2cd7acf 100644 --- a/app/Entities/Queries/EntityQueries.php +++ b/app/Entities/Queries/EntityQueries.php @@ -35,6 +35,7 @@ class EntityQueries /** * Start a query of visible entities of the given type, * suitable for listing display. + * @return Builder */ public function visibleForList(string $entityType): Builder { @@ -44,7 +45,6 @@ class EntityQueries protected function getQueriesForType(string $type): ProvidesEntityQueries { - /** @var ?ProvidesEntityQueries $queries */ $queries = match ($type) { 'page' => $this->pages, 'chapter' => $this->chapters, diff --git a/app/Entities/Queries/ProvidesEntityQueries.php b/app/Entities/Queries/ProvidesEntityQueries.php index 611d0ae52..1f8a71ae2 100644 --- a/app/Entities/Queries/ProvidesEntityQueries.php +++ b/app/Entities/Queries/ProvidesEntityQueries.php @@ -22,13 +22,14 @@ interface ProvidesEntityQueries public function start(): Builder; /** - * Find the entity of the given ID, or return null if not found. + * Find the entity of the given ID or return null if not found. */ public function findVisibleById(int $id): ?Entity; /** * Start a query for items that are visible, with selection * configured for list display of this item. + * @return Builder */ public function visibleForList(): Builder; } diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index f4d05d8af..bfc01a58d 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -89,12 +89,10 @@ class BaseRepo /** * Update the given items' cover image, or clear it. * - * @param Entity&CoverImageInterface $entity - * * @throws ImageUploadException * @throws \Exception */ - public function updateCoverImage($entity, ?UploadedFile $coverImage, bool $removeImage = false) + public function updateCoverImage(Entity&CoverImageInterface $entity, ?UploadedFile $coverImage, bool $removeImage = false) { if ($coverImage) { $imageType = $entity->coverImageTypeKey(); @@ -106,7 +104,7 @@ class BaseRepo if ($removeImage) { $this->imageRepo->destroyImage($entity->cover()->first()); - $entity->image_id = 0; + $entity->cover()->dissociate(); $entity->save(); } } diff --git a/app/Entities/Tools/SlugGenerator.php b/app/Entities/Tools/SlugGenerator.php index 5df300bb0..fb9123187 100644 --- a/app/Entities/Tools/SlugGenerator.php +++ b/app/Entities/Tools/SlugGenerator.php @@ -3,7 +3,7 @@ namespace BookStack\Entities\Tools; use BookStack\App\Model; -use BookStack\App\Sluggable; +use BookStack\App\SluggableInterface; use BookStack\Entities\Models\BookChild; use Illuminate\Support\Str; @@ -13,9 +13,9 @@ class SlugGenerator * Generate a fresh slug for the given entity. * The slug will be generated so that it doesn't conflict within the same parent item. */ - public function generate(Sluggable $model): string + public function generate(SluggableInterface&Model $model, string $slugSource): string { - $slug = $this->formatNameAsSlug($model->name); + $slug = $this->formatNameAsSlug($slugSource); while ($this->slugInUse($slug, $model)) { $slug .= '-' . Str::random(3); } @@ -24,7 +24,7 @@ class SlugGenerator } /** - * Format a name as a url slug. + * Format a name as a URL slug. */ protected function formatNameAsSlug(string $name): string { @@ -39,10 +39,8 @@ class SlugGenerator /** * Check if a slug is already in-use for this * type of model within the same parent. - * - * @param Sluggable&Model $model */ - protected function slugInUse(string $slug, Sluggable $model): bool + protected function slugInUse(string $slug, SluggableInterface&Model $model): bool { $query = $model->newQuery()->where('slug', '=', $slug); diff --git a/app/Permissions/PermissionApplicator.php b/app/Permissions/PermissionApplicator.php index ce4a543fd..e59b8ab67 100644 --- a/app/Permissions/PermissionApplicator.php +++ b/app/Permissions/PermissionApplicator.php @@ -7,8 +7,7 @@ use BookStack\Entities\EntityProvider; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; use BookStack\Permissions\Models\EntityPermission; -use BookStack\Users\Models\HasCreatorAndUpdater; -use BookStack\Users\Models\HasOwner; +use BookStack\Users\Models\OwnableInterface; use BookStack\Users\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -24,10 +23,8 @@ class PermissionApplicator /** * Checks if an entity has a restriction set upon it. - * - * @param Model&(HasCreatorAndUpdater|HasOwner) $ownable */ - public function checkOwnableUserAccess(Model $ownable, string $permission): bool + public function checkOwnableUserAccess(Model&OwnableInterface $ownable, string $permission): bool { $explodedPermission = explode('-', $permission); $action = $explodedPermission[1] ?? $explodedPermission[0]; @@ -39,7 +36,7 @@ class PermissionApplicator $allRolePermission = $user->can($fullPermission . '-all'); $ownRolePermission = $user->can($fullPermission . '-own'); $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment']; - $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by'; + $ownerField = $ownable->getOwnerFieldName(); $ownableFieldVal = $ownable->getAttribute($ownerField); if (is_null($ownableFieldVal)) { @@ -49,11 +46,15 @@ class PermissionApplicator $isOwner = $user->id === $ownableFieldVal; $hasRolePermission = $allRolePermission || ($isOwner && $ownRolePermission); - // Handle non entity specific jointPermissions + // Handle non-entity-specific jointPermissions if (in_array($explodedPermission[0], $nonJointPermissions)) { return $hasRolePermission; } + if (!($ownable instanceof Entity)) { + return false; + } + $hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action); return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions; diff --git a/app/References/CrossLinkParser.php b/app/References/CrossLinkParser.php index 1fd4c1b3e..3fb00be84 100644 --- a/app/References/CrossLinkParser.php +++ b/app/References/CrossLinkParser.php @@ -48,7 +48,7 @@ class CrossLinkParser /** * Get a list of href values from the given document. * - * @returns string[] + * @return string[] */ protected function getLinksFromContent(string $html): array { diff --git a/app/Search/SearchIndex.php b/app/Search/SearchIndex.php index 844e3584b..117d069ea 100644 --- a/app/Search/SearchIndex.php +++ b/app/Search/SearchIndex.php @@ -119,7 +119,7 @@ class SearchIndex * Create a scored term array from the given text, where the keys are the terms * and the values are their scores. * - * @returns array + * @return array */ protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array { @@ -136,7 +136,7 @@ class SearchIndex * Create a scored term array from the given HTML, where the keys are the terms * and the values are their scores. * - * @returns array + * @return array */ protected function generateTermScoreMapFromHtml(string $html): array { @@ -177,7 +177,7 @@ class SearchIndex * * @param Tag[] $tags * - * @returns array + * @return array */ protected function generateTermScoreMapFromTags(array $tags): array { @@ -199,7 +199,7 @@ class SearchIndex * For the given text, return an array where the keys are the unique term words * and the values are the frequency of that term. * - * @returns array + * @return array */ protected function textToTermCountMap(string $text): array { @@ -243,7 +243,7 @@ class SearchIndex * For the given entity, Generate an array of term data details. * Is the raw term data, not instances of SearchTerm models. * - * @returns array{term: string, score: float, entity_id: int, entity_type: string}[] + * @return array{term: string, score: float, entity_id: int, entity_type: string}[] */ protected function entityToTermDataArray(Entity $entity): array { @@ -279,7 +279,7 @@ class SearchIndex * * @param array[] ...$scoreMaps * - * @returns array + * @return array */ protected function mergeTermScoreMaps(...$scoreMaps): array { diff --git a/app/Sorting/BookSorter.php b/app/Sorting/BookSorter.php index cf41a6a94..e627d66fd 100644 --- a/app/Sorting/BookSorter.php +++ b/app/Sorting/BookSorter.php @@ -78,7 +78,7 @@ class BookSorter * Sort the books content using the given sort map. * Returns a list of books that were involved in the operation. * - * @returns Book[] + * @return Book[] */ public function sortUsingMap(BookSortMap $sortMap): array { diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 2d4900c96..44630acae 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -62,7 +62,7 @@ class ThemeEvents * * @param string $authSystem * @param array $userData - * @returns bool|null + * @return bool|null */ const AUTH_PRE_REGISTER = 'auth_pre_register'; @@ -83,7 +83,7 @@ class ThemeEvents * If the listener returns a non-null value, that will be used as an environment instead. * * @param \League\CommonMark\Environment\Environment $environment - * @returns \League\CommonMark\Environment\Environment|null + * @return \League\CommonMark\Environment\Environment|null */ const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure'; @@ -96,7 +96,7 @@ class ThemeEvents * * @param array $idTokenData * @param array $accessTokenData - * @returns array|null + * @return array|null */ const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate'; @@ -142,7 +142,7 @@ class ThemeEvents * Return values, if provided, will be used as a new response to use. * * @param \Illuminate\Http\Request $request - * @returns \Illuminate\Http\Response|null + * @return \Illuminate\Http\Response|null */ const WEB_MIDDLEWARE_BEFORE = 'web_middleware_before'; @@ -154,7 +154,7 @@ class ThemeEvents * * @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse $response - * @returns \Illuminate\Http\Response|null + * @return \Illuminate\Http\Response|null */ const WEB_MIDDLEWARE_AFTER = 'web_middleware_after'; @@ -173,7 +173,7 @@ class ThemeEvents * @param string|\BookStack\Activity\Models\Loggable $detail * @param \BookStack\Users\Models\User $initiator * @param int $initiatedTime - * @returns array|null + * @return array|null */ const WEBHOOK_CALL_BEFORE = 'webhook_call_before'; } diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php index 57d7cb334..05227243a 100644 --- a/app/Uploads/Attachment.php +++ b/app/Uploads/Attachment.php @@ -8,6 +8,7 @@ use BookStack\Entities\Models\Page; use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\PermissionApplicator; use BookStack\Users\Models\HasCreatorAndUpdater; +use BookStack\Users\Models\OwnableInterface; use BookStack\Users\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -27,7 +28,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * * @method static Entity|Builder visible() */ -class Attachment extends Model +class Attachment extends Model implements OwnableInterface { use HasCreatorAndUpdater; use HasFactory; diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 0a267a644..ec8f8be0f 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -7,6 +7,7 @@ use BookStack\Entities\Models\Page; use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\PermissionApplicator; use BookStack\Users\Models\HasCreatorAndUpdater; +use BookStack\Users\Models\OwnableInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -21,7 +22,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property int $created_by * @property int $updated_by */ -class Image extends Model +class Image extends Model implements OwnableInterface { use HasFactory; use HasCreatorAndUpdater; diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index a8f144517..458f0102d 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -138,7 +138,7 @@ class ImageService * Get the raw data content from an image. * * @throws Exception - * @returns ?resource + * @return ?resource */ public function getImageStream(Image $image): mixed { diff --git a/app/Uploads/ImageStorageDisk.php b/app/Uploads/ImageStorageDisk.php index f2667d993..36d6721de 100644 --- a/app/Uploads/ImageStorageDisk.php +++ b/app/Uploads/ImageStorageDisk.php @@ -61,7 +61,7 @@ class ImageStorageDisk /** * Get a stream to the file at the given path. - * @returns ?resource + * @return ?resource */ public function stream(string $path): mixed { diff --git a/app/Users/Controllers/UserApiController.php b/app/Users/Controllers/UserApiController.php index bb2570b31..351893b03 100644 --- a/app/Users/Controllers/UserApiController.php +++ b/app/Users/Controllers/UserApiController.php @@ -81,7 +81,7 @@ class UserApiController extends ApiController return $this->apiListingResponse($users, [ 'id', 'name', 'slug', 'email', 'external_auth_id', 'created_at', 'updated_at', 'last_activity_at', - ], [Closure::fromCallable([$this, 'listFormatter'])]); + ], [$this->listFormatter(...)]); } /** diff --git a/app/Users/Models/HasCreatorAndUpdater.php b/app/Users/Models/HasCreatorAndUpdater.php index 9fb24ead9..bb05de11a 100644 --- a/app/Users/Models/HasCreatorAndUpdater.php +++ b/app/Users/Models/HasCreatorAndUpdater.php @@ -27,4 +27,9 @@ trait HasCreatorAndUpdater { return $this->belongsTo(User::class, 'updated_by'); } + + public function getOwnerFieldName(): string + { + return 'created_by'; + } } diff --git a/app/Users/Models/HasOwner.php b/app/Users/Models/HasOwner.php deleted file mode 100644 index eb830ef6f..000000000 --- a/app/Users/Models/HasOwner.php +++ /dev/null @@ -1,19 +0,0 @@ -belongsTo(User::class, 'owned_by'); - } -} diff --git a/app/Users/Models/OwnableInterface.php b/app/Users/Models/OwnableInterface.php new file mode 100644 index 000000000..8f738487f --- /dev/null +++ b/app/Users/Models/OwnableInterface.php @@ -0,0 +1,8 @@ + */ public function permissions(): BelongsToMany { diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index 0d437418b..b6989ce2d 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -10,7 +10,7 @@ use BookStack\Activity\Models\Loggable; use BookStack\Activity\Models\Watch; use BookStack\Api\ApiToken; use BookStack\App\Model; -use BookStack\App\Sluggable; +use BookStack\App\SluggableInterface; use BookStack\Entities\Tools\SlugGenerator; use BookStack\Translation\LocaleDefinition; use BookStack\Translation\LocaleManager; @@ -47,7 +47,7 @@ use Illuminate\Support\Collection; * @property Collection $mfaValues * @property ?Image $avatar */ -class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable +class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, SluggableInterface { use HasFactory; use Authenticatable; @@ -372,7 +372,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function refreshSlug(): string { - $this->slug = app()->make(SlugGenerator::class)->generate($this); + $this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name); return $this->slug; } diff --git a/phpcs.xml b/phpcs.xml index 8d4c6b702..8337d5aac 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -14,7 +14,7 @@ - + ./tests/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0f2021383..9dfd9d29e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,7 +7,7 @@ parameters: - app # The level 8 is the highest level - level: 1 + level: 2 phpVersion: min: 80200 From e05ec7da361027126e538bbe5908d0bfdf1ede4a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Sep 2025 10:47:45 +0100 Subject: [PATCH 021/139] Maintenance: Addressed a range of phpstan level 3 issues --- app/Access/ExternalBaseUserProvider.php | 24 +--- .../Guards/ExternalBaseSessionGuard.php | 111 ++++-------------- app/Activity/Models/Comment.php | 2 +- app/App/Providers/AuthServiceProvider.php | 4 +- app/App/Providers/EventServiceProvider.php | 2 +- .../Controllers/PageRevisionController.php | 2 +- app/Entities/Models/PageRevision.php | 2 +- app/Exceptions/Handler.php | 4 +- app/Exports/ImportRepo.php | 3 + .../ZipExports/Models/ZipExportAttachment.php | 6 +- .../ZipExports/Models/ZipExportBook.php | 6 +- .../ZipExports/Models/ZipExportChapter.php | 6 +- .../ZipExports/Models/ZipExportImage.php | 6 +- .../ZipExports/Models/ZipExportModel.php | 4 +- .../ZipExports/Models/ZipExportPage.php | 6 +- .../ZipExports/Models/ZipExportTag.php | 6 +- app/Http/Kernel.php | 6 +- app/Http/Middleware/EncryptCookies.php | 2 +- .../PreventRequestsDuringMaintenance.php | 2 +- app/Http/Middleware/TrimStrings.php | 2 +- app/Http/Middleware/TrustProxies.php | 2 +- app/Http/Middleware/VerifyCsrfToken.php | 2 +- app/Search/SearchOptionSet.php | 3 + app/Search/SearchRunner.php | 2 +- app/Users/Models/User.php | 13 +- app/Util/OutOfMemoryHandler.php | 7 +- 26 files changed, 84 insertions(+), 151 deletions(-) diff --git a/app/Access/ExternalBaseUserProvider.php b/app/Access/ExternalBaseUserProvider.php index 2b5ddfbf3..2165fd459 100644 --- a/app/Access/ExternalBaseUserProvider.php +++ b/app/Access/ExternalBaseUserProvider.php @@ -2,33 +2,18 @@ namespace BookStack\Access; +use BookStack\Users\Models\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; -use Illuminate\Database\Eloquent\Model; class ExternalBaseUserProvider implements UserProvider { - public function __construct( - protected string $model - ) { - } - - /** - * Create a new instance of the model. - */ - public function createModel(): Model - { - $class = '\\' . ltrim($this->model, '\\'); - - return new $class(); - } - /** * Retrieve a user by their unique identifier. */ public function retrieveById(mixed $identifier): ?Authenticatable { - return $this->createModel()->newQuery()->find($identifier); + return User::query()->find($identifier); } /** @@ -59,10 +44,7 @@ class ExternalBaseUserProvider implements UserProvider */ public function retrieveByCredentials(array $credentials): ?Authenticatable { - // Search current user base by looking up a uid - $model = $this->createModel(); - - return $model->newQuery() + return User::query() ->where('external_auth_id', $credentials['external_auth_id']) ->first(); } diff --git a/app/Access/Guards/ExternalBaseSessionGuard.php b/app/Access/Guards/ExternalBaseSessionGuard.php index 2d15422a5..899e93c36 100644 --- a/app/Access/Guards/ExternalBaseSessionGuard.php +++ b/app/Access/Guards/ExternalBaseSessionGuard.php @@ -4,7 +4,7 @@ namespace BookStack\Access\Guards; use BookStack\Access\RegistrationService; use Illuminate\Auth\GuardHelpers; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; +use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Session\Session; @@ -24,43 +24,31 @@ class ExternalBaseSessionGuard implements StatefulGuard * The name of the Guard. Typically "session". * * Corresponds to guard name in authentication configuration. - * - * @var string */ - protected $name; + protected readonly string $name; /** * The user we last attempted to retrieve. - * - * @var \Illuminate\Contracts\Auth\Authenticatable */ - protected $lastAttempted; + protected Authenticatable $lastAttempted; /** * The session used by the guard. - * - * @var \Illuminate\Contracts\Session\Session */ - protected $session; + protected Session $session; /** * Indicates if the logout method has been called. - * - * @var bool */ - protected $loggedOut = false; + protected bool $loggedOut = false; /** * Service to handle common registration actions. - * - * @var RegistrationService */ - protected $registrationService; + protected RegistrationService $registrationService; /** * Create a new authentication guard. - * - * @return void */ public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService) { @@ -72,13 +60,11 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Get the currently authenticated user. - * - * @return \Illuminate\Contracts\Auth\Authenticatable|null */ - public function user() + public function user(): Authenticatable|null { if ($this->loggedOut) { - return; + return null; } // If we've already retrieved the user for the current request we can just @@ -101,13 +87,11 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Get the ID for the currently authenticated user. - * - * @return int|null */ - public function id() + public function id(): int|null { if ($this->loggedOut) { - return; + return null; } return $this->user() @@ -117,12 +101,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log a user into the application without sessions or cookies. - * - * @param array $credentials - * - * @return bool */ - public function once(array $credentials = []) + public function once(array $credentials = []): bool { if ($this->validate($credentials)) { $this->setUser($this->lastAttempted); @@ -135,12 +115,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log the given user ID into the application without sessions or cookies. - * - * @param mixed $id - * - * @return \Illuminate\Contracts\Auth\Authenticatable|false */ - public function onceUsingId($id) + public function onceUsingId($id): Authenticatable|false { if (!is_null($user = $this->provider->retrieveById($id))) { $this->setUser($user); @@ -153,38 +129,26 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Validate a user's credentials. - * - * @param array $credentials - * - * @return bool */ - public function validate(array $credentials = []) + public function validate(array $credentials = []): bool { return false; } /** * Attempt to authenticate a user using the given credentials. - * - * @param array $credentials - * @param bool $remember - * - * @return bool + * @param bool $remember */ - public function attempt(array $credentials = [], $remember = false) + public function attempt(array $credentials = [], $remember = false): bool { return false; } /** * Log the given user ID into the application. - * - * @param mixed $id * @param bool $remember - * - * @return \Illuminate\Contracts\Auth\Authenticatable|false */ - public function loginUsingId($id, $remember = false) + public function loginUsingId(mixed $id, $remember = false): Authenticatable|false { // Always return false as to disable this method, // Logins should route through LoginService. @@ -194,12 +158,9 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log a user into the application. * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * @param bool $remember - * - * @return void + * @param bool $remember */ - public function login(AuthenticatableContract $user, $remember = false) + public function login(Authenticatable $user, $remember = false): void { $this->updateSession($user->getAuthIdentifier()); @@ -208,12 +169,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Update the session with the given ID. - * - * @param string $id - * - * @return void */ - protected function updateSession($id) + protected function updateSession(string|int $id): void { $this->session->put($this->getName(), $id); @@ -222,10 +179,8 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Log the user out of the application. - * - * @return void */ - public function logout() + public function logout(): void { $this->clearUserDataFromStorage(); @@ -239,62 +194,48 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Remove the user data from the session and cookies. - * - * @return void */ - protected function clearUserDataFromStorage() + protected function clearUserDataFromStorage(): void { $this->session->remove($this->getName()); } /** * Get the last user we attempted to authenticate. - * - * @return \Illuminate\Contracts\Auth\Authenticatable */ - public function getLastAttempted() + public function getLastAttempted(): Authenticatable { return $this->lastAttempted; } /** * Get a unique identifier for the auth session value. - * - * @return string */ - public function getName() + public function getName(): string { return 'login_' . $this->name . '_' . sha1(static::class); } /** * Determine if the user was authenticated via "remember me" cookie. - * - * @return bool */ - public function viaRemember() + public function viaRemember(): bool { return false; } /** * Return the currently cached user. - * - * @return \Illuminate\Contracts\Auth\Authenticatable|null */ - public function getUser() + public function getUser(): Authenticatable|null { return $this->user; } /** * Set the current user. - * - * @param \Illuminate\Contracts\Auth\Authenticatable $user - * - * @return $this */ - public function setUser(AuthenticatableContract $user) + public function setUser(Authenticatable $user): self { $this->user = $user; diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index 6b05a9bab..0cb83d61e 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -39,7 +39,7 @@ class Comment extends Model implements Loggable, OwnableInterface /** * Get the parent comment this is in reply to (if existing). - * @return BelongsTo + * @return BelongsTo */ public function parent(): BelongsTo { diff --git a/app/App/Providers/AuthServiceProvider.php b/app/App/Providers/AuthServiceProvider.php index 23c339079..6a8162521 100644 --- a/app/App/Providers/AuthServiceProvider.php +++ b/app/App/Providers/AuthServiceProvider.php @@ -59,8 +59,8 @@ class AuthServiceProvider extends ServiceProvider */ public function register(): void { - Auth::provider('external-users', function ($app, array $config) { - return new ExternalBaseUserProvider($config['model']); + Auth::provider('external-users', function () { + return new ExternalBaseUserProvider(); }); // Bind and provide the default system user as a singleton to the app instance when needed. diff --git a/app/App/Providers/EventServiceProvider.php b/app/App/Providers/EventServiceProvider.php index 34ab7cfef..60e78efe0 100644 --- a/app/App/Providers/EventServiceProvider.php +++ b/app/App/Providers/EventServiceProvider.php @@ -15,7 +15,7 @@ class EventServiceProvider extends ServiceProvider /** * The event listener mappings for the application. * - * @var array> + * @var array> */ protected $listen = [ SocialiteWasCalled::class => [ diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index 4985c39f3..c43eea08b 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -98,7 +98,7 @@ class PageRevisionController extends Controller throw new NotFoundException(); } - $prev = $revision->getPrevious(); + $prev = $revision->getPreviousRevision(); $prevContent = $prev->html ?? ''; $diff = Diff::excecute($prevContent, $revision->html); diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php index 10ff6d901..1a6c980e1 100644 --- a/app/Entities/Models/PageRevision.php +++ b/app/Entities/Models/PageRevision.php @@ -60,7 +60,7 @@ class PageRevision extends Model implements Loggable /** * Get the previous revision for the same page if existing. */ - public function getPrevious(): ?PageRevision + public function getPreviousRevision(): ?PageRevision { $id = static::newQuery()->where('page_id', '=', $this->page_id) ->where('id', '<', $this->id) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 61e126327..b8c3fa192 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -94,7 +94,7 @@ class Handler extends ExceptionHandler * If the callable returns a response, this response will be returned * to the request upon error. */ - public function prepareForOutOfMemory(callable $onOutOfMemory) + public function prepareForOutOfMemory(callable $onOutOfMemory): void { $this->onOutOfMemory = $onOutOfMemory; } @@ -102,7 +102,7 @@ class Handler extends ExceptionHandler /** * Forget the current out of memory handler, if existing. */ - public function forgetOutOfMemoryHandler() + public function forgetOutOfMemoryHandler(): void { $this->onOutOfMemory = null; } diff --git a/app/Exports/ImportRepo.php b/app/Exports/ImportRepo.php index e030a88d2..896af903a 100644 --- a/app/Exports/ImportRepo.php +++ b/app/Exports/ImportRepo.php @@ -39,6 +39,9 @@ class ImportRepo return $this->queryVisible()->get(); } + /** + * @return Builder + */ public function queryVisible(): Builder { $query = Import::query(); diff --git a/app/Exports/ZipExports/Models/ZipExportAttachment.php b/app/Exports/ZipExports/Models/ZipExportAttachment.php index 4f5b2f236..97995738f 100644 --- a/app/Exports/ZipExports/Models/ZipExportAttachment.php +++ b/app/Exports/ZipExports/Models/ZipExportAttachment.php @@ -6,7 +6,7 @@ use BookStack\Exports\ZipExports\ZipExportFiles; use BookStack\Exports\ZipExports\ZipValidationHelper; use BookStack\Uploads\Attachment; -class ZipExportAttachment extends ZipExportModel +final class ZipExportAttachment extends ZipExportModel { public ?int $id = null; public string $name; @@ -52,9 +52,9 @@ class ZipExportAttachment extends ZipExportModel return $context->validateData($data, $rules); } - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - $model = new self(); + $model = new static(); $model->id = $data['id'] ?? null; $model->name = $data['name']; diff --git a/app/Exports/ZipExports/Models/ZipExportBook.php b/app/Exports/ZipExports/Models/ZipExportBook.php index 39176ded4..6c51ea337 100644 --- a/app/Exports/ZipExports/Models/ZipExportBook.php +++ b/app/Exports/ZipExports/Models/ZipExportBook.php @@ -8,7 +8,7 @@ use BookStack\Entities\Models\Page; use BookStack\Exports\ZipExports\ZipExportFiles; use BookStack\Exports\ZipExports\ZipValidationHelper; -class ZipExportBook extends ZipExportModel +final class ZipExportBook extends ZipExportModel { public ?int $id = null; public string $name; @@ -101,9 +101,9 @@ class ZipExportBook extends ZipExportModel return $errors; } - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - $model = new self(); + $model = new static(); $model->id = $data['id'] ?? null; $model->name = $data['name']; diff --git a/app/Exports/ZipExports/Models/ZipExportChapter.php b/app/Exports/ZipExports/Models/ZipExportChapter.php index bf2dc78f8..260191a3e 100644 --- a/app/Exports/ZipExports/Models/ZipExportChapter.php +++ b/app/Exports/ZipExports/Models/ZipExportChapter.php @@ -7,7 +7,7 @@ use BookStack\Entities\Models\Page; use BookStack\Exports\ZipExports\ZipExportFiles; use BookStack\Exports\ZipExports\ZipValidationHelper; -class ZipExportChapter extends ZipExportModel +final class ZipExportChapter extends ZipExportModel { public ?int $id = null; public string $name; @@ -79,9 +79,9 @@ class ZipExportChapter extends ZipExportModel return $errors; } - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - $model = new self(); + $model = new static(); $model->id = $data['id'] ?? null; $model->name = $data['name']; diff --git a/app/Exports/ZipExports/Models/ZipExportImage.php b/app/Exports/ZipExports/Models/ZipExportImage.php index e0e7d1198..4c71af0c3 100644 --- a/app/Exports/ZipExports/Models/ZipExportImage.php +++ b/app/Exports/ZipExports/Models/ZipExportImage.php @@ -7,7 +7,7 @@ use BookStack\Exports\ZipExports\ZipValidationHelper; use BookStack\Uploads\Image; use Illuminate\Validation\Rule; -class ZipExportImage extends ZipExportModel +final class ZipExportImage extends ZipExportModel { public ?int $id = null; public string $name; @@ -43,9 +43,9 @@ class ZipExportImage extends ZipExportModel return $context->validateData($data, $rules); } - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - $model = new self(); + $model = new static(); $model->id = $data['id'] ?? null; $model->name = $data['name']; diff --git a/app/Exports/ZipExports/Models/ZipExportModel.php b/app/Exports/ZipExports/Models/ZipExportModel.php index d3a8c3567..38001c628 100644 --- a/app/Exports/ZipExports/Models/ZipExportModel.php +++ b/app/Exports/ZipExports/Models/ZipExportModel.php @@ -30,12 +30,12 @@ abstract class ZipExportModel implements JsonSerializable /** * Decode the array of data into this export model. */ - abstract public static function fromArray(array $data): self; + abstract public static function fromArray(array $data): static; /** * Decode an array of array data into an array of export models. * @param array[] $data - * @return self[] + * @return static[] */ public static function fromManyArray(array $data): array { diff --git a/app/Exports/ZipExports/Models/ZipExportPage.php b/app/Exports/ZipExports/Models/ZipExportPage.php index 097443df0..6de7f9446 100644 --- a/app/Exports/ZipExports/Models/ZipExportPage.php +++ b/app/Exports/ZipExports/Models/ZipExportPage.php @@ -7,7 +7,7 @@ use BookStack\Entities\Tools\PageContent; use BookStack\Exports\ZipExports\ZipExportFiles; use BookStack\Exports\ZipExports\ZipValidationHelper; -class ZipExportPage extends ZipExportModel +final class ZipExportPage extends ZipExportModel { public ?int $id = null; public string $name; @@ -86,9 +86,9 @@ class ZipExportPage extends ZipExportModel return $errors; } - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - $model = new self(); + $model = new static(); $model->id = $data['id'] ?? null; $model->name = $data['name']; diff --git a/app/Exports/ZipExports/Models/ZipExportTag.php b/app/Exports/ZipExports/Models/ZipExportTag.php index 6b4720fca..8ac7f3c4d 100644 --- a/app/Exports/ZipExports/Models/ZipExportTag.php +++ b/app/Exports/ZipExports/Models/ZipExportTag.php @@ -5,7 +5,7 @@ namespace BookStack\Exports\ZipExports\Models; use BookStack\Activity\Models\Tag; use BookStack\Exports\ZipExports\ZipValidationHelper; -class ZipExportTag extends ZipExportModel +final class ZipExportTag extends ZipExportModel { public string $name; public ?string $value = null; @@ -39,9 +39,9 @@ class ZipExportTag extends ZipExportModel return $context->validateData($data, $rules); } - public static function fromArray(array $data): self + public static function fromArray(array $data): static { - $model = new self(); + $model = new static(); $model->name = $data['name']; $model->value = $data['value'] ?? null; diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 30714e2ac..00bf8cbe1 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -9,6 +9,8 @@ class Kernel extends HttpKernel /** * The application's global HTTP middleware stack. * These middleware are run during every request to your application. + * + * @var list */ protected $middleware = [ \BookStack\Http\Middleware\PreventRequestsDuringMaintenance::class, @@ -21,7 +23,7 @@ class Kernel extends HttpKernel /** * The application's route middleware groups. * - * @var array + * @var array> */ protected $middlewareGroups = [ 'web' => [ @@ -47,7 +49,7 @@ class Kernel extends HttpKernel /** * The application's middleware aliases. * - * @var array + * @var array */ protected $middlewareAliases = [ 'auth' => \BookStack\Http\Middleware\Authenticate::class, diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php index 484c15ae8..2d7d5d9d4 100644 --- a/app/Http/Middleware/EncryptCookies.php +++ b/app/Http/Middleware/EncryptCookies.php @@ -9,7 +9,7 @@ class EncryptCookies extends Middleware /** * The names of the cookies that should not be encrypted. * - * @var array + * @var array */ protected $except = [ // diff --git a/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/app/Http/Middleware/PreventRequestsDuringMaintenance.php index dfb9592e1..c1a02c8cc 100644 --- a/app/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -9,7 +9,7 @@ class PreventRequestsDuringMaintenance extends Middleware /** * The URIs that should be reachable while maintenance mode is enabled. * - * @var array + * @var array */ protected $except = [ // diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php index cbdc88fb9..ec5725d67 100644 --- a/app/Http/Middleware/TrimStrings.php +++ b/app/Http/Middleware/TrimStrings.php @@ -9,7 +9,7 @@ class TrimStrings extends Middleware /** * The names of the attributes that should not be trimmed. * - * @var array + * @var array */ protected $except = [ 'password', diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index 0fe0247b8..ecf6cb18a 100644 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -11,7 +11,7 @@ class TrustProxies extends Middleware /** * The trusted proxies for this application. * - * @var array + * @var array|string|null */ protected $proxies; diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 804a22bc0..7c8689b8b 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -16,7 +16,7 @@ class VerifyCsrfToken extends Middleware /** * The URIs that should be excluded from CSRF verification. * - * @var array + * @var array */ protected $except = [ 'saml2/*', diff --git a/app/Search/SearchOptionSet.php b/app/Search/SearchOptionSet.php index bd5e5a5b2..844d145e6 100644 --- a/app/Search/SearchOptionSet.php +++ b/app/Search/SearchOptionSet.php @@ -14,6 +14,9 @@ class SearchOptionSet */ protected array $options = []; + /** + * @param T[] $options + */ public function __construct(array $options = []) { $this->options = $options; diff --git a/app/Search/SearchRunner.php b/app/Search/SearchRunner.php index 9716f8053..c14d1c642 100644 --- a/app/Search/SearchRunner.php +++ b/app/Search/SearchRunner.php @@ -285,7 +285,7 @@ class SearchRunner * * @param array $termCounts * - * @return array + * @return array */ protected function rawTermCountsToAdjustments(array $termCounts): array { diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index b6989ce2d..f83e12088 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -26,6 +26,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; @@ -64,7 +65,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * The attributes that are mass assignable. * - * @var array + * @var list */ protected $fillable = ['name', 'email']; @@ -73,7 +74,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * The attributes excluded from the model's JSON form. * - * @var array + * @var list */ protected $hidden = [ 'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email', @@ -118,14 +119,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * The roles that belong to the user. * - * @return BelongsToMany + * @return BelongsToMany */ - public function roles() + public function roles(): BelongsToMany { - if ($this->id === 0) { - return; - } - return $this->belongsToMany(Role::class); } diff --git a/app/Util/OutOfMemoryHandler.php b/app/Util/OutOfMemoryHandler.php index 88e9581f4..6632d22a5 100644 --- a/app/Util/OutOfMemoryHandler.php +++ b/app/Util/OutOfMemoryHandler.php @@ -42,7 +42,7 @@ class OutOfMemoryHandler } /** - * Forget the handler so no action is taken place on out of memory. + * Forget the handler, so no action is taken place on out of memory. */ public function forget(): void { @@ -53,6 +53,11 @@ class OutOfMemoryHandler protected function getHandler(): Handler { + /** + * We want to resolve our specific BookStack handling via the set app handler + * singleton, but phpstan will only infer based on the interface. + * @phpstan-ignore return.type + */ return app()->make(ExceptionHandler::class); } } From 318b486e0b7601a00dc02bd1adcef22066bfef5d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Sep 2025 15:18:49 +0100 Subject: [PATCH 022/139] Maintenance: Finished changes to meet phpstan level 3 --- .../Guards/AsyncExternalBaseSessionGuard.php | 20 ++++-------- app/Access/Guards/LdapSessionGuard.php | 6 +--- app/Access/LoginService.php | 10 +++--- app/Entities/Queries/BookQueries.php | 3 ++ app/Entities/Queries/BookshelfQueries.php | 3 ++ app/Entities/Queries/ChapterQueries.php | 3 ++ app/Entities/Queries/PageQueries.php | 6 ++++ .../Queries/ProvidesEntityQueries.php | 7 +++-- app/Entities/Tools/PageIncludeParser.php | 6 ++-- app/Exceptions/Handler.php | 31 +++++++------------ app/Permissions/EntityPermissionEvaluator.php | 4 +-- 11 files changed, 48 insertions(+), 51 deletions(-) diff --git a/app/Access/Guards/AsyncExternalBaseSessionGuard.php b/app/Access/Guards/AsyncExternalBaseSessionGuard.php index ab982a6b0..b66fbe95e 100644 --- a/app/Access/Guards/AsyncExternalBaseSessionGuard.php +++ b/app/Access/Guards/AsyncExternalBaseSessionGuard.php @@ -3,23 +3,18 @@ namespace BookStack\Access\Guards; /** - * Saml2 Session Guard. + * External Auth Session Guard. * - * The saml2 login process is async in nature meaning it does not fit very well - * into the default laravel 'Guard' auth flow. Instead most of the logic is done - * via the Saml2 controller & Saml2Service. This class provides a safer, thin - * version of SessionGuard. + * The login process for external auth (SAML2/OIDC) is async in nature, meaning it does not fit very well + * into the default laravel 'Guard' auth flow. Instead, most of the logic is done via the relevant + * controller and services. This class provides a safer, thin version of SessionGuard. */ class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard { /** * Validate a user's credentials. - * - * @param array $credentials - * - * @return bool */ - public function validate(array $credentials = []) + public function validate(array $credentials = []): bool { return false; } @@ -27,12 +22,9 @@ class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard /** * Attempt to authenticate a user using the given credentials. * - * @param array $credentials * @param bool $remember - * - * @return bool */ - public function attempt(array $credentials = [], $remember = false) + public function attempt(array $credentials = [], $remember = false): bool { return false; } diff --git a/app/Access/Guards/LdapSessionGuard.php b/app/Access/Guards/LdapSessionGuard.php index bd020f70b..9455d530d 100644 --- a/app/Access/Guards/LdapSessionGuard.php +++ b/app/Access/Guards/LdapSessionGuard.php @@ -35,13 +35,9 @@ class LdapSessionGuard extends ExternalBaseSessionGuard /** * Validate a user's credentials. * - * @param array $credentials - * * @throws LdapException - * - * @return bool */ - public function validate(array $credentials = []) + public function validate(array $credentials = []): bool { $userDetails = $this->ldapService->getUserDetails($credentials['username']); diff --git a/app/Access/LoginService.php b/app/Access/LoginService.php index 6607969af..76ae1938d 100644 --- a/app/Access/LoginService.php +++ b/app/Access/LoginService.php @@ -95,7 +95,7 @@ class LoginService { $value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY); if (!$value) { - return ['user_id' => null, 'method' => null]; + return ['user_id' => null, 'method' => null, 'remember' => false]; } [$id, $method, $remember, $time] = explode(':', $value); @@ -103,18 +103,18 @@ class LoginService if ($time < $hourAgo) { $this->clearLastLoginAttempted(); - return ['user_id' => null, 'method' => null]; + return ['user_id' => null, 'method' => null, 'remember' => false]; } return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)]; } /** - * Set the last login attempted user. + * Set the last login-attempted user. * Must be only used when credentials are correct and a login could be - * achieved but a secondary factor has stopped the login. + * achieved, but a secondary factor has stopped the login. */ - protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember) + protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember): void { session()->put( self::LAST_LOGIN_ATTEMPTED_SESSION_KEY, diff --git a/app/Entities/Queries/BookQueries.php b/app/Entities/Queries/BookQueries.php index 5ff22252c..2492f8131 100644 --- a/app/Entities/Queries/BookQueries.php +++ b/app/Entities/Queries/BookQueries.php @@ -6,6 +6,9 @@ use BookStack\Entities\Models\Book; use BookStack\Exceptions\NotFoundException; use Illuminate\Database\Eloquent\Builder; +/** + * @implements ProvidesEntityQueries + */ class BookQueries implements ProvidesEntityQueries { protected static array $listAttributes = [ diff --git a/app/Entities/Queries/BookshelfQueries.php b/app/Entities/Queries/BookshelfQueries.php index 874bc7f2f..842011a87 100644 --- a/app/Entities/Queries/BookshelfQueries.php +++ b/app/Entities/Queries/BookshelfQueries.php @@ -6,6 +6,9 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Exceptions\NotFoundException; use Illuminate\Database\Eloquent\Builder; +/** + * @implements ProvidesEntityQueries + */ class BookshelfQueries implements ProvidesEntityQueries { protected static array $listAttributes = [ diff --git a/app/Entities/Queries/ChapterQueries.php b/app/Entities/Queries/ChapterQueries.php index 53c5bc9d8..9bf0ff65b 100644 --- a/app/Entities/Queries/ChapterQueries.php +++ b/app/Entities/Queries/ChapterQueries.php @@ -6,6 +6,9 @@ use BookStack\Entities\Models\Chapter; use BookStack\Exceptions\NotFoundException; use Illuminate\Database\Eloquent\Builder; +/** + * @implements ProvidesEntityQueries + */ class ChapterQueries implements ProvidesEntityQueries { protected static array $listAttributes = [ diff --git a/app/Entities/Queries/PageQueries.php b/app/Entities/Queries/PageQueries.php index f821ee86a..ee7b201bc 100644 --- a/app/Entities/Queries/PageQueries.php +++ b/app/Entities/Queries/PageQueries.php @@ -6,6 +6,9 @@ use BookStack\Entities\Models\Page; use BookStack\Exceptions\NotFoundException; use Illuminate\Database\Eloquent\Builder; +/** + * @implements ProvidesEntityQueries + */ class PageQueries implements ProvidesEntityQueries { protected static array $contentAttributes = [ @@ -18,6 +21,9 @@ class PageQueries implements ProvidesEntityQueries 'template', 'text', 'created_at', 'updated_at', 'priority', 'owned_by', ]; + /** + * @return Builder + */ public function start(): Builder { return Page::query(); diff --git a/app/Entities/Queries/ProvidesEntityQueries.php b/app/Entities/Queries/ProvidesEntityQueries.php index 1f8a71ae2..79fc64b3a 100644 --- a/app/Entities/Queries/ProvidesEntityQueries.php +++ b/app/Entities/Queries/ProvidesEntityQueries.php @@ -7,17 +7,20 @@ use Illuminate\Database\Eloquent\Builder; /** * Interface for our classes which provide common queries for our - * entity objects. Ideally all queries for entities should run through + * entity objects. Ideally, all queries for entities should run through * these classes. * Any added methods should return a builder instances to allow extension * via building on the query, unless the method starts with 'find' * in which case an entity object should be returned. * (nullable unless it's a *OrFail method). + * + * @template TModel of Entity */ interface ProvidesEntityQueries { /** * Start a new query for this entity type. + * @return Builder */ public function start(): Builder; @@ -29,7 +32,7 @@ interface ProvidesEntityQueries /** * Start a query for items that are visible, with selection * configured for list display of this item. - * @return Builder + * @return Builder */ public function visibleForList(): Builder; } diff --git a/app/Entities/Tools/PageIncludeParser.php b/app/Entities/Tools/PageIncludeParser.php index 329a7633f..af7ed4fc6 100644 --- a/app/Entities/Tools/PageIncludeParser.php +++ b/app/Entities/Tools/PageIncludeParser.php @@ -13,8 +13,8 @@ class PageIncludeParser protected static string $includeTagRegex = "/{{@\s?([0-9].*?)}}/"; /** - * Elements to clean up and remove if left empty after a parsing operation. - * @var DOMElement[] + * Nodes to clean up and remove if left empty after a parsing operation. + * @var DOMNode[] */ protected array $toCleanup = []; @@ -206,7 +206,7 @@ class PageIncludeParser } /** - * Cleanup after a parse operation. + * Clean up after a parse operation. * Removes stranded elements we may have left during the parse. */ protected function cleanup(): void diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b8c3fa192..08d326ad8 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,7 +2,6 @@ namespace BookStack\Exceptions; -use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; @@ -12,6 +11,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Validation\ValidationException; use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Throwable; @@ -20,7 +20,7 @@ class Handler extends ExceptionHandler /** * A list of the exception types that are not reported. * - * @var array> + * @var array> */ protected $dontReport = [ NotFoundException::class, @@ -50,11 +50,11 @@ class Handler extends ExceptionHandler /** * Report or log an exception. * - * @param \Throwable $exception - * - * @throws \Throwable + * @param Throwable $exception * * @return void + *@throws Throwable + * */ public function report(Throwable $exception) { @@ -64,12 +64,9 @@ class Handler extends ExceptionHandler /** * Render an exception into an HTTP response. * - * @param \Illuminate\Http\Request $request - * @param Exception $e - * - * @return \Illuminate\Http\Response + * @param Request $request */ - public function render($request, Throwable $e) + public function render($request, Throwable $e): SymfonyResponse { if ($e instanceof FatalError && str_contains($e->getMessage(), 'bytes exhausted (tried to allocate') && $this->onOutOfMemory) { $response = call_user_func($this->onOutOfMemory); @@ -152,12 +149,9 @@ class Handler extends ExceptionHandler /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Auth\AuthenticationException $exception - * - * @return \Illuminate\Http\Response + * @param Request $request */ - protected function unauthenticated($request, AuthenticationException $exception) + protected function unauthenticated($request, AuthenticationException $exception): SymfonyResponse { if ($request->expectsJson()) { return response()->json(['error' => 'Unauthenticated.'], 401); @@ -169,12 +163,9 @@ class Handler extends ExceptionHandler /** * Convert a validation exception into a JSON response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Validation\ValidationException $exception - * - * @return \Illuminate\Http\JsonResponse + * @param Request $request */ - protected function invalidJson($request, ValidationException $exception) + protected function invalidJson($request, ValidationException $exception): JsonResponse { return response()->json($exception->errors(), $exception->status); } diff --git a/app/Permissions/EntityPermissionEvaluator.php b/app/Permissions/EntityPermissionEvaluator.php index 98ec03306..30c97b700 100644 --- a/app/Permissions/EntityPermissionEvaluator.php +++ b/app/Permissions/EntityPermissionEvaluator.php @@ -30,7 +30,7 @@ class EntityPermissionEvaluator } /** - * @param array> $permitsByType + * @param array> $permitsByType */ protected function evaluatePermitsByType(array $permitsByType): ?int { @@ -50,7 +50,7 @@ class EntityPermissionEvaluator /** * @param string[] $typeIdChain * @param array $permissionMapByTypeId - * @return array> + * @return array> */ protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array { From 7d1c31620213599fdc07ce5cda776d91f5642afc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 3 Sep 2025 15:42:50 +0100 Subject: [PATCH 023/139] Maintenance: Updated larastan target level, fixed issues from tests --- app/Access/Guards/ExternalBaseSessionGuard.php | 2 +- app/Console/Commands/UpdateUrlCommand.php | 2 +- app/Entities/Controllers/RecycleBinApiController.php | 7 ++++--- phpstan.neon.dist | 2 +- tests/Entity/CommentStoreTest.php | 1 - 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Access/Guards/ExternalBaseSessionGuard.php b/app/Access/Guards/ExternalBaseSessionGuard.php index 899e93c36..91239599b 100644 --- a/app/Access/Guards/ExternalBaseSessionGuard.php +++ b/app/Access/Guards/ExternalBaseSessionGuard.php @@ -30,7 +30,7 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * The user we last attempted to retrieve. */ - protected Authenticatable $lastAttempted; + protected Authenticatable|null $lastAttempted; /** * The session used by the guard. diff --git a/app/Console/Commands/UpdateUrlCommand.php b/app/Console/Commands/UpdateUrlCommand.php index e155878d3..71f0b92fe 100644 --- a/app/Console/Commands/UpdateUrlCommand.php +++ b/app/Console/Commands/UpdateUrlCommand.php @@ -52,7 +52,7 @@ class UpdateUrlCommand extends Command 'page_revisions' => ['html', 'text', 'markdown'], 'images' => ['url'], 'settings' => ['value'], - 'comments' => ['html', 'text'], + 'comments' => ['html'], ]; foreach ($columnsToUpdateByTable as $table => $columns) { diff --git a/app/Entities/Controllers/RecycleBinApiController.php b/app/Entities/Controllers/RecycleBinApiController.php index fdc24ddf8..89bd3f41a 100644 --- a/app/Entities/Controllers/RecycleBinApiController.php +++ b/app/Entities/Controllers/RecycleBinApiController.php @@ -9,6 +9,7 @@ use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\DeletionRepo; use BookStack\Http\ApiController; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\HasMany; class RecycleBinApiController extends ApiController @@ -69,7 +70,7 @@ class RecycleBinApiController extends ApiController /** * Load some related details for the deletion listing. */ - protected function listFormatter(Deletion $deletion) + protected function listFormatter(Deletion $deletion): void { $deletable = $deletion->deletable; @@ -89,9 +90,9 @@ class RecycleBinApiController extends ApiController } /** - * @param HasMany $query + * @param Builder $query */ - protected static function withTrashedQuery(HasMany $query): void + protected static function withTrashedQuery(Builder $query): void { $query->withTrashed(); } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9dfd9d29e..72189222f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,7 +7,7 @@ parameters: - app # The level 8 is the highest level - level: 2 + level: 3 phpVersion: min: 80200 diff --git a/tests/Entity/CommentStoreTest.php b/tests/Entity/CommentStoreTest.php index c5fe4ce50..de093a3a6 100644 --- a/tests/Entity/CommentStoreTest.php +++ b/tests/Entity/CommentStoreTest.php @@ -27,7 +27,6 @@ class CommentStoreTest extends TestCase 'local_id' => 1, 'entity_id' => $page->id, 'entity_type' => Page::newModelInstance()->getMorphClass(), - 'text' => null, 'parent_id' => 2, ]); From 579c1bf4241270f96f527101f89a2c90c9b9cb6b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Sep 2025 15:06:58 +0100 Subject: [PATCH 024/139] Timezones: Seperated out store & display timezones to two options --- .env.example.complete | 8 +++- .../Providers/ViewTweaksServiceProvider.php | 13 +++++++ app/Config/app.php | 8 ++-- app/Util/DateFormatter.php | 25 +++++++++++++ resources/views/entities/meta.blade.php | 8 ++-- tests/Util/DateFormatterTest.php | 37 +++++++++++++++++++ 6 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 app/Util/DateFormatter.php create mode 100644 tests/Util/DateFormatterTest.php diff --git a/.env.example.complete b/.env.example.complete index 25687aaac..18e7bd00d 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -36,10 +36,14 @@ APP_LANG=en # APP_LANG will be used if such a header is not provided. APP_AUTO_LANG_PUBLIC=true -# Application timezone -# Used where dates are displayed such as on exported content. +# Application timezones +# The first option is used to determine what timezone is used for date storage. +# Leaving that as "UTC" is advised. +# The second option is used to set the timezone which will be used for date +# formatting and display. This defaults to the "APP_TIMEZONE" value. # Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php APP_TIMEZONE=UTC +APP_DISPLAY_TIMEZONE=UTC # Application theme # Used to specific a themes/ folder where BookStack UI diff --git a/app/App/Providers/ViewTweaksServiceProvider.php b/app/App/Providers/ViewTweaksServiceProvider.php index 7115dcb51..6771e513f 100644 --- a/app/App/Providers/ViewTweaksServiceProvider.php +++ b/app/App/Providers/ViewTweaksServiceProvider.php @@ -3,6 +3,7 @@ namespace BookStack\App\Providers; use BookStack\Entities\BreadcrumbsViewComposer; +use BookStack\Util\DateFormatter; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\View; @@ -10,6 +11,15 @@ use Illuminate\Support\ServiceProvider; class ViewTweaksServiceProvider extends ServiceProvider { + public function register() + { + $this->app->singleton(DateFormatter::class, function ($app) { + return new DateFormatter( + $app['config']->get('app.display_timezone'), + ); + }); + } + /** * Bootstrap services. */ @@ -21,6 +31,9 @@ class ViewTweaksServiceProvider extends ServiceProvider // View Composers View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class); + // View Globals + View::share('dates', $this->app->make(DateFormatter::class)); + // Custom blade view directives Blade::directive('icon', function ($expression) { return "toHtml(); ?>"; diff --git a/app/Config/app.php b/app/Config/app.php index b96d0bdb7..40e542d3e 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -70,8 +70,8 @@ return [ // A list of the sources/hostnames that can be reached by application SSR calls. // This is used wherever users can provide URLs/hosts in-platform, like for webhooks. // Host-specific functionality (usually controlled via other options) like auth - // or user avatars for example, won't use this list. - // Space seperated if multiple. Can use '*' as a wildcard. + // or user avatars, for example, won't use this list. + // Space separated if multiple. Can use '*' as a wildcard. // Values will be compared prefix-matched, case-insensitive, against called SSR urls. // Defaults to allow all hosts. 'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'), @@ -80,8 +80,10 @@ return [ // Integer value between 0 (IP hidden) to 4 (Full IP usage) 'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4), - // Application timezone for back-end date functions. + // Application timezone for stored date/time values. 'timezone' => env('APP_TIMEZONE', 'UTC'), + // Application timezone for displayed date/time values in the UI. + 'display_timezone' => env('APP_DISPLAY_TIMEZONE', env('APP_TIMEZONE', 'UTC')), // Default locale to use // A default variant is also stored since Laravel can overwrite diff --git a/app/Util/DateFormatter.php b/app/Util/DateFormatter.php new file mode 100644 index 000000000..489ed54e2 --- /dev/null +++ b/app/Util/DateFormatter.php @@ -0,0 +1,25 @@ +clone()->setTimezone($this->displayTimezone); + + return $withDisplayTimezone->format('Y-m-d H:i:s T'); + } + + public function relative(Carbon $date): string + { + return $date->diffForHumans(); + } +} diff --git a/resources/views/entities/meta.blade.php b/resources/views/entities/meta.blade.php index 9d3c4b956..c9d301aa3 100644 --- a/resources/views/entities/meta.blade.php +++ b/resources/views/entities/meta.blade.php @@ -31,7 +31,7 @@ @icon('star')
{!! trans('entities.meta_created_name', [ - 'timeLength' => ''.$entity->created_at->diffForHumans() . '', + 'timeLength' => ''. $dates->relative($entity->created_at) . '', 'user' => "".e($entity->createdBy->name). "" ]) !!}
@@ -39,7 +39,7 @@ @else
@icon('star') - {{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }} + {{ trans('entities.meta_created', ['timeLength' => $dates->relative($entity->created_at)]) }}
@endif @@ -48,7 +48,7 @@ @icon('edit')
{!! trans('entities.meta_updated_name', [ - 'timeLength' => '' . $entity->updated_at->diffForHumans() .'', + 'timeLength' => '' . $dates->relative($entity->updated_at) .'', 'user' => "".e($entity->updatedBy->name). "" ]) !!}
@@ -56,7 +56,7 @@ @elseif (!$entity->isA('revision'))
@icon('edit') - {{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }} + {{ trans('entities.meta_updated', ['timeLength' => $dates->relative($entity->updated_at)]) }}
@endif diff --git a/tests/Util/DateFormatterTest.php b/tests/Util/DateFormatterTest.php new file mode 100644 index 000000000..c9004b5a5 --- /dev/null +++ b/tests/Util/DateFormatterTest.php @@ -0,0 +1,37 @@ +isoWithTimezone($dateTime); + $this->assertEquals('2020-06-01 13:00:00 BST', $result); + } + + public function test_iso_with_timezone_works_from_non_utc_dates() + { + $formatter = new DateFormatter('Asia/Shanghai'); + $dateTime = new Carbon('2025-06-10 15:25:00', 'America/New_York'); + + $result = $formatter->isoWithTimezone($dateTime); + $this->assertEquals('2025-06-11 03:25:00 CST', $result); + } + + public function test_relative() + { + $formatter = new DateFormatter('Europe/London'); + $dateTime = (new Carbon('now', 'UTC'))->subMinutes(50); + + $result = $formatter->relative($dateTime); + $this->assertEquals('50 minutes ago', $result); + } +} From 36cb243d5e47afbdf6c7c847bed5c34454abfa48 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 4 Sep 2025 16:11:35 +0100 Subject: [PATCH 025/139] Timezones: Updated date displays to use consistent formats --- app/Entities/Tools/PageEditActivity.php | 19 ++--- app/Util/DateFormatter.php | 7 +- phpunit.xml | 2 + resources/views/comments/comment.blade.php | 84 +++++++++++-------- .../views/common/activity-item.blade.php | 2 +- resources/views/entities/grid-item.blade.php | 4 +- resources/views/entities/list-item.blade.php | 4 +- resources/views/entities/meta.blade.php | 8 +- resources/views/exports/import-show.blade.php | 2 +- .../views/exports/parts/import.blade.php | 2 +- resources/views/exports/parts/meta.blade.php | 4 +- .../pages/parts/image-manager-form.blade.php | 8 +- .../pages/parts/revisions-index-row.blade.php | 4 +- .../parts/template-manager-list.blade.php | 2 +- .../views/settings/webhooks/edit.blade.php | 12 ++- .../views/users/api-tokens/edit.blade.php | 8 +- .../users/parts/users-list-item.blade.php | 2 +- resources/views/users/profile.blade.php | 2 +- tests/Exports/HtmlExportTest.php | 4 +- tests/Util/DateFormatterTest.php | 4 +- 20 files changed, 100 insertions(+), 84 deletions(-) diff --git a/app/Entities/Tools/PageEditActivity.php b/app/Entities/Tools/PageEditActivity.php index 646b200f1..22f89bf62 100644 --- a/app/Entities/Tools/PageEditActivity.php +++ b/app/Entities/Tools/PageEditActivity.php @@ -4,19 +4,15 @@ namespace BookStack\Entities\Tools; use BookStack\Entities\Models\Page; use BookStack\Entities\Models\PageRevision; +use BookStack\Util\DateFormatter; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; class PageEditActivity { - protected Page $page; - - /** - * PageEditActivity constructor. - */ - public function __construct(Page $page) - { - $this->page = $page; + public function __construct( + protected Page $page + ) { } /** @@ -50,11 +46,9 @@ class PageEditActivity /** * Get any editor clash warning messages to show for the given draft revision. * - * @param PageRevision|Page $draft - * * @return string[] */ - public function getWarningMessagesForDraft($draft): array + public function getWarningMessagesForDraft(Page|PageRevision $draft): array { $warnings = []; @@ -82,7 +76,8 @@ class PageEditActivity */ public function getEditingActiveDraftMessage(PageRevision $draft): string { - $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]); + $formatter = resolve(DateFormatter::class); + $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $formatter->relative($draft->updated_at)]); if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) { return $message; } diff --git a/app/Util/DateFormatter.php b/app/Util/DateFormatter.php index 489ed54e2..c6e60bd53 100644 --- a/app/Util/DateFormatter.php +++ b/app/Util/DateFormatter.php @@ -3,6 +3,7 @@ namespace BookStack\Util; use Carbon\Carbon; +use Carbon\CarbonInterface; class DateFormatter { @@ -11,15 +12,15 @@ class DateFormatter ) { } - public function isoWithTimezone(Carbon $date): string + public function absolute(Carbon $date): string { $withDisplayTimezone = $date->clone()->setTimezone($this->displayTimezone); return $withDisplayTimezone->format('Y-m-d H:i:s T'); } - public function relative(Carbon $date): string + public function relative(Carbon $date, bool $includeSuffix = true): string { - return $date->diffForHumans(); + return $date->diffForHumans(null, $includeSuffix ? null : CarbonInterface::DIFF_ABSOLUTE); } } diff --git a/phpunit.xml b/phpunit.xml index a8e725d41..8a7ab9cb7 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -16,6 +16,8 @@ + + diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index d70a8c1d9..67aac7203 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -14,7 +14,8 @@
@if ($comment->createdBy)
- {{ $comment->createdBy->name }} + {{ $comment->createdBy->name }}
@endif
@@ -23,50 +24,55 @@ @else {{ trans('common.deleted_user') }} @endif -  {{ trans('entities.comment_created', ['createDiff' => $comment->created_at->diffForHumans() ]) }} +  {{ trans('entities.comment_created', ['createDiff' => $dates->relative($comment->created_at) ]) }} @if($comment->isUpdated()) - + {{ trans('entities.comment_updated_indicator') }} @endif
@if(!$readOnly && (userCan('comment-create-all') || userCan('comment-update', $comment) || userCan('comment-delete', $comment))) -
- @if(userCan('comment-create-all')) - - @endif - @if(!$comment->parent_id && (userCan('comment-update', $comment) || userCan('comment-delete', $comment))) - - @endif - @if(userCan('comment-update', $comment)) - - @endif - @if(userCan('comment-delete', $comment)) - - @endif - +
+ @if(userCan('comment-create-all')) + + @endif + @if(!$comment->parent_id && (userCan('comment-update', $comment) || userCan('comment-delete', $comment))) + + @endif + @if(userCan('comment-update', $comment)) + + @endif + @if(userCan('comment-delete', $comment)) + + @endif +  •  -
+
@endif
@@ -76,7 +82,8 @@
@if ($comment->parent_id)

- @icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }} + @icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}

@endif @if($comment->content_ref) @@ -86,7 +93,8 @@ option:page-comment-reference:view-comment-text="{{ trans('entities.comment_view') }}" option:page-comment-reference:jump-to-thread-text="{{ trans('entities.comment_jump_to_thread') }}" option:page-comment-reference:close-text="{{ trans('common.close') }}" - href="#">@icon('bookmark'){{ trans('entities.comment_reference') }} {{ trans('entities.comment_reference_outdated') }} + href="#">@icon('bookmark'){{ trans('entities.comment_reference') }} + {{ trans('entities.comment_reference_outdated') }}
@endif {!! $commentHtml !!} @@ -95,10 +103,12 @@ @if(!$readOnly && userCan('comment-update', $comment)) diff --git a/resources/views/common/activity-item.blade.php b/resources/views/common/activity-item.blade.php index 1c970084f..1d3c7bd75 100644 --- a/resources/views/common/activity-item.blade.php +++ b/resources/views/common/activity-item.blade.php @@ -26,5 +26,5 @@
- @icon('time'){{ $activity->created_at->diffForHumans() }} + @icon('time'){{ $dates->relative($activity->created_at) }}
diff --git a/resources/views/entities/grid-item.blade.php b/resources/views/entities/grid-item.blade.php index ee31b53f2..17c54e263 100644 --- a/resources/views/entities/grid-item.blade.php +++ b/resources/views/entities/grid-item.blade.php @@ -10,7 +10,7 @@

{{ $entity->getExcerpt(130) }}

\ No newline at end of file diff --git a/resources/views/entities/list-item.blade.php b/resources/views/entities/list-item.blade.php index 2fadef191..5174de431 100644 --- a/resources/views/entities/list-item.blade.php +++ b/resources/views/entities/list-item.blade.php @@ -27,9 +27,9 @@ @endif @if(($showUpdatedBy ?? false) && $entity->relationLoaded('updatedBy') && $entity->updatedBy) - + {!! trans('entities.meta_updated_name', [ - 'timeLength' => $entity->updated_at->diffForHumans(), + 'timeLength' => $dates->relative($entity->updated_at), 'user' => e($entity->updatedBy->name) ]) !!} diff --git a/resources/views/entities/meta.blade.php b/resources/views/entities/meta.blade.php index c9d301aa3..060c197a4 100644 --- a/resources/views/entities/meta.blade.php +++ b/resources/views/entities/meta.blade.php @@ -31,7 +31,7 @@ @icon('star')
{!! trans('entities.meta_created_name', [ - 'timeLength' => ''. $dates->relative($entity->created_at) . '', + 'timeLength' => ''. $dates->relative($entity->created_at) . '', 'user' => "".e($entity->createdBy->name). "" ]) !!}
@@ -39,7 +39,7 @@ @else
@icon('star') - {{ trans('entities.meta_created', ['timeLength' => $dates->relative($entity->created_at)]) }} + {{ trans('entities.meta_created', ['timeLength' => $dates->relative($entity->created_at)]) }}
@endif @@ -48,7 +48,7 @@ @icon('edit')
{!! trans('entities.meta_updated_name', [ - 'timeLength' => '' . $dates->relative($entity->updated_at) .'', + 'timeLength' => '' . $dates->relative($entity->updated_at) .'', 'user' => "".e($entity->updatedBy->name). "" ]) !!}
@@ -56,7 +56,7 @@ @elseif (!$entity->isA('revision'))
@icon('edit') - {{ trans('entities.meta_updated', ['timeLength' => $dates->relative($entity->updated_at)]) }} + {{ trans('entities.meta_updated', ['timeLength' => $dates->relative($entity->updated_at)]) }}
@endif diff --git a/resources/views/exports/import-show.blade.php b/resources/views/exports/import-show.blade.php index a28b79bb3..1c46b7a0b 100644 --- a/resources/views/exports/import-show.blade.php +++ b/resources/views/exports/import-show.blade.php @@ -26,7 +26,7 @@
{{ trans('entities.import_size', ['size' => $import->getSizeString()]) }}
-
{{ trans('entities.import_uploaded_at', ['relativeTime' => $import->created_at->diffForHumans()]) }}
+
{{ trans('entities.import_uploaded_at', ['relativeTime' => $dates->relative($import->created_at)]) }}
@if($import->createdBy)
{{ trans('entities.import_uploaded_by') }} diff --git a/resources/views/exports/parts/import.blade.php b/resources/views/exports/parts/import.blade.php index 2f7659c46..dc287370e 100644 --- a/resources/views/exports/parts/import.blade.php +++ b/resources/views/exports/parts/import.blade.php @@ -5,6 +5,6 @@
{{ $import->getSizeString() }}
-
@icon('time'){{ $import->created_at->diffForHumans() }}
+
@icon('time'){{ $dates->relative($import->created_at) }}
\ No newline at end of file diff --git a/resources/views/exports/parts/meta.blade.php b/resources/views/exports/parts/meta.blade.php index d4128898b..00117f4a1 100644 --- a/resources/views/exports/parts/meta.blade.php +++ b/resources/views/exports/parts/meta.blade.php @@ -4,13 +4,13 @@ @endif @icon('star'){!! trans('entities.meta_created' . ($entity->createdBy ? '_name' : ''), [ - 'timeLength' => $entity->created_at->isoFormat('D MMMM Y HH:mm:ss'), + 'timeLength' => $dates->absolute($entity->created_at), 'user' => e($entity->createdBy->name ?? ''), ]) !!}
@icon('edit'){!! trans('entities.meta_updated' . ($entity->updatedBy ? '_name' : ''), [ - 'timeLength' => $entity->updated_at->isoFormat('D MMMM Y HH:mm:ss'), + 'timeLength' => $dates->absolute($entity->updated_at), 'user' => e($entity->updatedBy->name ?? '') ]) !!} \ No newline at end of file diff --git a/resources/views/pages/parts/image-manager-form.blade.php b/resources/views/pages/parts/image-manager-form.blade.php index bd84e247d..452a0aaa1 100644 --- a/resources/views/pages/parts/image-manager-form.blade.php +++ b/resources/views/pages/parts/image-manager-form.blade.php @@ -94,12 +94,12 @@

-
- @icon('star') {{ trans('components.image_uploaded', ['uploadedDate' => $image->created_at->diffForHumans()]) }} +
+ @icon('star') {{ trans('components.image_uploaded', ['uploadedDate' => $dates->relative($image->created_at)]) }}
@if($image->created_at->valueOf() !== $image->updated_at->valueOf()) -
- @icon('edit') {{ trans('components.image_updated', ['updateDate' => $image->updated_at->diffForHumans()]) }} +
+ @icon('edit') {{ trans('components.image_updated', ['updateDate' => $dates->relative($image->updated_at)]) }}
@endif @if($image->createdBy) diff --git a/resources/views/pages/parts/revisions-index-row.blade.php b/resources/views/pages/parts/revisions-index-row.blade.php index 48bea5b57..19b924763 100644 --- a/resources/views/pages/parts/revisions-index-row.blade.php +++ b/resources/views/pages/parts/revisions-index-row.blade.php @@ -17,8 +17,8 @@ @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
- {{ $revision->created_at->isoFormat('D MMMM Y HH:mm:ss') }} - ({{ $revision->created_at->diffForHumans() }}) + {{ $dates->absolute($revision->created_at) }} + ({{ $dates->relative($revision->created_at) }})
diff --git a/resources/views/pages/parts/template-manager-list.blade.php b/resources/views/pages/parts/template-manager-list.blade.php index f2f70c142..990583a71 100644 --- a/resources/views/pages/parts/template-manager-list.blade.php +++ b/resources/views/pages/parts/template-manager-list.blade.php @@ -6,7 +6,7 @@ draggable="true" template-id="{{ $template->id }}">
{{ $template->name }}
-
{{ trans('entities.meta_updated', ['timeLength' => $template->updated_at->diffForHumans()]) }}
+
{{ trans('entities.meta_updated', ['timeLength' => $dates->relative($template->updated_at)]) }}
diff --git a/resources/views/users/api-tokens/edit.blade.php b/resources/views/users/api-tokens/edit.blade.php index aa3e49ded..3a1ff49d3 100644 --- a/resources/views/users/api-tokens/edit.blade.php +++ b/resources/views/users/api-tokens/edit.blade.php @@ -42,12 +42,12 @@
- - {{ trans('settings.user_api_token_created', ['timeAgo' => $token->created_at->diffForHumans()]) }} + + {{ trans('settings.user_api_token_created', ['timeAgo' => $dates->relative($token->created_at)]) }}
- - {{ trans('settings.user_api_token_updated', ['timeAgo' => $token->created_at->diffForHumans()]) }} + + {{ trans('settings.user_api_token_updated', ['timeAgo' => $dates->relative($token->created_at)]) }}
diff --git a/resources/views/users/parts/users-list-item.blade.php b/resources/views/users/parts/users-list-item.blade.php index dc7c9f272..9c7ecd147 100644 --- a/resources/views/users/parts/users-list-item.blade.php +++ b/resources/views/users/parts/users-list-item.blade.php @@ -20,7 +20,7 @@ @if($user->last_activity_at) {{ trans('settings.users_latest_activity') }}
- {{ $user->last_activity_at->diffForHumans() }} + {{ $dates->relative($user->last_activity_at) }} @endif
diff --git a/resources/views/users/profile.blade.php b/resources/views/users/profile.blade.php index a8be8a4c1..9879091a5 100644 --- a/resources/views/users/profile.blade.php +++ b/resources/views/users/profile.blade.php @@ -23,7 +23,7 @@

{{ $user->name }}

- {{ trans('entities.profile_user_for_x', ['time' => $user->created_at->diffForHumans(null, true)]) }} + {{ trans('entities.profile_user_for_x', ['time' => $dates->relative($user->created_at, false)]) }}

diff --git a/tests/Exports/HtmlExportTest.php b/tests/Exports/HtmlExportTest.php index 069cf2801..e039fb2cc 100644 --- a/tests/Exports/HtmlExportTest.php +++ b/tests/Exports/HtmlExportTest.php @@ -99,9 +99,9 @@ class HtmlExportTest extends TestCase $page = $this->entities->page(); $resp = $this->asEditor()->get($page->getUrl('/export/html')); - $resp->assertSee($page->created_at->isoFormat('D MMMM Y HH:mm:ss')); + $resp->assertSee($page->created_at->format('Y-m-d H:i:s T')); $resp->assertDontSee($page->created_at->diffForHumans()); - $resp->assertSee($page->updated_at->isoFormat('D MMMM Y HH:mm:ss')); + $resp->assertSee($page->updated_at->format('Y-m-d H:i:s T')); $resp->assertDontSee($page->updated_at->diffForHumans()); } diff --git a/tests/Util/DateFormatterTest.php b/tests/Util/DateFormatterTest.php index c9004b5a5..1c0a458e0 100644 --- a/tests/Util/DateFormatterTest.php +++ b/tests/Util/DateFormatterTest.php @@ -13,7 +13,7 @@ class DateFormatterTest extends TestCase $formatter = new DateFormatter('Europe/London'); $dateTime = new Carbon('2020-06-01 12:00:00', 'UTC'); - $result = $formatter->isoWithTimezone($dateTime); + $result = $formatter->absolute($dateTime); $this->assertEquals('2020-06-01 13:00:00 BST', $result); } @@ -22,7 +22,7 @@ class DateFormatterTest extends TestCase $formatter = new DateFormatter('Asia/Shanghai'); $dateTime = new Carbon('2025-06-10 15:25:00', 'America/New_York'); - $result = $formatter->isoWithTimezone($dateTime); + $result = $formatter->absolute($dateTime); $this->assertEquals('2025-06-11 03:25:00 CST', $result); } From c8716df284e74fad780ed8d36b23669d725b164a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Sep 2025 15:59:25 +0100 Subject: [PATCH 026/139] Permissions: Removed unused role-perm columns, added permission enum Updated main permission check methods to support our new enum. --- app/App/helpers.php | 3 +- app/Entities/Tools/PermissionsUpdater.php | 11 +- app/Permissions/Models/EntityPermission.php | 2 - app/Permissions/Models/RolePermission.php | 1 - app/Permissions/Permission.php | 151 ++++++++++++++++++ app/Permissions/PermissionApplicator.php | 16 +- app/Users/Models/Role.php | 3 +- app/Users/Models/User.php | 21 ++- ...25_09_02_111542_remove_unused_columns.php} | 5 + tests/Helpers/PermissionsProvider.php | 5 +- 10 files changed, 190 insertions(+), 28 deletions(-) create mode 100644 app/Permissions/Permission.php rename database/migrations/{2025_09_02_111542_remove_comments_text_column.php => 2025_09_02_111542_remove_unused_columns.php} (76%) diff --git a/app/App/helpers.php b/app/App/helpers.php index 2305c2d72..0e26d9974 100644 --- a/app/App/helpers.php +++ b/app/App/helpers.php @@ -3,6 +3,7 @@ use BookStack\App\AppVersion; use BookStack\App\Model; use BookStack\Facades\Theme; +use BookStack\Permissions\Permission; use BookStack\Permissions\PermissionApplicator; use BookStack\Settings\SettingService; use BookStack\Users\Models\User; @@ -39,7 +40,7 @@ function user(): User * Check if the current user has a permission. If an ownable element * is passed in the jointPermissions are checked against that particular item. */ -function userCan(string $permission, ?Model $ownable = null): bool +function userCan(string|Permission $permission, ?Model $ownable = null): bool { if (is_null($ownable)) { return user()->can($permission); diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index 9f3b8f952..4ca53982a 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -8,6 +8,7 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Models\Entity; use BookStack\Facades\Activity; use BookStack\Permissions\Models\EntityPermission; +use BookStack\Permissions\Permission; use BookStack\Users\Models\Role; use BookStack\Users\Models\User; use Illuminate\Http\Request; @@ -93,8 +94,9 @@ class PermissionsUpdater foreach ($permissions as $roleId => $info) { $entityPermissionData = ['role_id' => $roleId]; - foreach (EntityPermission::PERMISSIONS as $permission) { - $entityPermissionData[$permission] = (($info[$permission] ?? false) === "true"); + foreach (Permission::genericForEntity() as $permission) { + $permName = $permission->value; + $entityPermissionData[$permName] = (($info[$permName] ?? false) === "true"); } $formatted[] = $entityPermissionData; } @@ -108,8 +110,9 @@ class PermissionsUpdater foreach ($permissions as $requestPermissionData) { $entityPermissionData = ['role_id' => $requestPermissionData['role_id']]; - foreach (EntityPermission::PERMISSIONS as $permission) { - $entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false); + foreach (Permission::genericForEntity() as $permission) { + $permName = $permission->value; + $entityPermissionData[$permName] = boolval($requestPermissionData[$permName] ?? false); } $formatted[] = $entityPermissionData; } diff --git a/app/Permissions/Models/EntityPermission.php b/app/Permissions/Models/EntityPermission.php index 1ef9c2886..efad2ba39 100644 --- a/app/Permissions/Models/EntityPermission.php +++ b/app/Permissions/Models/EntityPermission.php @@ -18,8 +18,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; */ class EntityPermission extends Model { - public const PERMISSIONS = ['view', 'create', 'update', 'delete']; - protected $fillable = ['role_id', 'view', 'create', 'update', 'delete']; public $timestamps = false; protected $hidden = ['entity_id', 'entity_type', 'id']; diff --git a/app/Permissions/Models/RolePermission.php b/app/Permissions/Models/RolePermission.php index d43313ea0..67b1c55f5 100644 --- a/app/Permissions/Models/RolePermission.php +++ b/app/Permissions/Models/RolePermission.php @@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * @property int $id * @property string $name - * @property string $display_name */ class RolePermission extends Model { diff --git a/app/Permissions/Permission.php b/app/Permissions/Permission.php new file mode 100644 index 000000000..539bbe808 --- /dev/null +++ b/app/Permissions/Permission.php @@ -0,0 +1,151 @@ +value; + $explodedPermission = explode('-', $permissionName); $action = $explodedPermission[1] ?? $explodedPermission[0]; - $fullPermission = count($explodedPermission) > 1 ? $permission : $ownable->getMorphClass() . '-' . $permission; + $fullPermission = count($explodedPermission) > 1 ? $permissionName : $ownable->getMorphClass() . '-' . $permissionName; $user = $this->currentUser(); $userRoleIds = $this->getCurrentUserRoleIds(); @@ -235,8 +236,13 @@ class PermissionApplicator */ protected function ensureValidEntityAction(string $action): void { - if (!in_array($action, EntityPermission::PERMISSIONS)) { - throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission'); + $allowed = Permission::genericForEntity(); + foreach ($allowed as $permission) { + if ($permission->value === $action) { + return; + } } + + throw new InvalidArgumentException('Action should be a simple entity permission action, not a role permission'); } } diff --git a/app/Users/Models/Role.php b/app/Users/Models/Role.php index 912ae81c9..92be22458 100644 --- a/app/Users/Models/Role.php +++ b/app/Users/Models/Role.php @@ -57,7 +57,8 @@ class Role extends Model implements Loggable */ public function permissions(): BelongsToMany { - return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id'); + return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id') + ->select(['id', 'name']); } /** diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index f83e12088..0017653b5 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -12,6 +12,7 @@ use BookStack\Api\ApiToken; use BookStack\App\Model; use BookStack\App\SluggableInterface; use BookStack\Entities\Tools\SlugGenerator; +use BookStack\Permissions\Permission; use BookStack\Translation\LocaleDefinition; use BookStack\Translation\LocaleManager; use BookStack\Uploads\Image; @@ -26,7 +27,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; @@ -156,8 +156,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Check if the user has a particular permission. */ - public function can(string $permissionName): bool + public function can(string|Permission $permission): bool { + $permissionName = is_string($permission) ? $permission : $permission->value; return $this->permissions()->contains($permissionName); } @@ -181,9 +182,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } /** - * Clear any cached permissions on this instance. + * Clear any cached permissions in this instance. */ - public function clearPermissionCache() + public function clearPermissionCache(): void { $this->permissions = null; } @@ -191,7 +192,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Attach a role to this user. */ - public function attachRole(Role $role) + public function attachRole(Role $role): void { $this->roles()->attach($role->id); $this->unsetRelation('roles'); @@ -207,15 +208,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Check if the user has a social account, - * If a driver is passed it checks for that single account type. - * - * @param bool|string $socialDriver - * - * @return bool + * If a driver is passed, it checks for that single account type. */ - public function hasSocialAccount($socialDriver = false) + public function hasSocialAccount(string $socialDriver = ''): bool { - if ($socialDriver === false) { + if (empty($socialDriver)) { return $this->socialAccounts()->count() > 0; } diff --git a/database/migrations/2025_09_02_111542_remove_comments_text_column.php b/database/migrations/2025_09_02_111542_remove_unused_columns.php similarity index 76% rename from database/migrations/2025_09_02_111542_remove_comments_text_column.php rename to database/migrations/2025_09_02_111542_remove_unused_columns.php index 8caa05e54..1ec2dfdf9 100644 --- a/database/migrations/2025_09_02_111542_remove_comments_text_column.php +++ b/database/migrations/2025_09_02_111542_remove_unused_columns.php @@ -14,6 +14,11 @@ return new class extends Migration Schema::table('comments', function (Blueprint $table) { $table->dropColumn('text'); }); + + Schema::table('role_permissions', function (Blueprint $table) { + $table->dropColumn('display_name'); + $table->dropColumn('description'); + }); } /** diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php index cb036fe97..c9ae30919 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -5,6 +5,7 @@ namespace Tests\Helpers; use BookStack\Entities\Models\Entity; use BookStack\Permissions\Models\EntityPermission; use BookStack\Permissions\Models\RolePermission; +use BookStack\Permissions\Permission; use BookStack\Settings\SettingService; use BookStack\Users\Models\Role; use BookStack\Users\Models\User; @@ -139,8 +140,8 @@ class PermissionsProvider protected function actionListToEntityPermissionData(array $actionList, int $roleId = 0): array { $permissionData = ['role_id' => $roleId]; - foreach (EntityPermission::PERMISSIONS as $possibleAction) { - $permissionData[$possibleAction] = in_array($possibleAction, $actionList); + foreach (Permission::genericForEntity() as $permission) { + $permissionData[$permission->value] = in_array($permission->value, $actionList); } return $permissionData; From 5fc11d46d5115b96a2571fa371671c26900fab8f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Sep 2025 16:15:42 +0100 Subject: [PATCH 027/139] Permissions: Added enum usage to controller helpers Also fixed various missing types or spelling/formatting points. Added down action for role_permission table changes in migration. --- app/Http/Controller.php | 27 +++++++++---------- app/Users/Controllers/RoleApiController.php | 13 ++++----- ...025_09_02_111542_remove_unused_columns.php | 5 ++++ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/app/Http/Controller.php b/app/Http/Controller.php index 7f2134dd8..5d3be4951 100644 --- a/app/Http/Controller.php +++ b/app/Http/Controller.php @@ -6,6 +6,7 @@ use BookStack\Activity\Models\Loggable; use BookStack\App\Model; use BookStack\Exceptions\NotifyException; use BookStack\Facades\Activity; +use BookStack\Permissions\Permission; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; @@ -27,10 +28,9 @@ abstract class Controller extends BaseController } /** - * Stops the application and shows a permission error if - * the application is in demo mode. + * Stops the application and shows a permission error if the application is in demo mode. */ - protected function preventAccessInDemoMode() + protected function preventAccessInDemoMode(): void { if (config('app.env') === 'demo') { $this->showPermissionError(); @@ -40,14 +40,13 @@ abstract class Controller extends BaseController /** * Adds the page title into the view. */ - public function setPageTitle(string $title) + public function setPageTitle(string $title): void { view()->share('pageTitle', $title); } /** - * On a permission error redirect to home and display. - * the error as a notification. + * On a permission error redirect to home and display the error as a notification. * * @throws NotifyException */ @@ -61,7 +60,7 @@ abstract class Controller extends BaseController /** * Checks that the current user has the given permission otherwise throw an exception. */ - protected function checkPermission(string $permission): void + protected function checkPermission(string|Permission $permission): void { if (!user() || !user()->can($permission)) { $this->showPermissionError(); @@ -81,7 +80,7 @@ abstract class Controller extends BaseController /** * Check the current user's permissions against an ownable item otherwise throw an exception. */ - protected function checkOwnablePermission(string $permission, Model $ownable, string $redirectLocation = '/'): void + protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void { if (!userCan($permission, $ownable)) { $this->showPermissionError($redirectLocation); @@ -92,7 +91,7 @@ abstract class Controller extends BaseController * Check if a user has a permission or bypass the permission * check if the given callback resolves true. */ - protected function checkPermissionOr(string $permission, callable $callback): void + protected function checkPermissionOr(string|Permission $permission, callable $callback): void { if ($callback() !== true) { $this->checkPermission($permission); @@ -103,7 +102,7 @@ abstract class Controller extends BaseController * Check if the current user has a permission or bypass if the provided user * id matches the current user. */ - protected function checkPermissionOrCurrentUser(string $permission, int $userId): void + protected function checkPermissionOrCurrentUser(string|Permission $permission, int $userId): void { $this->checkPermissionOr($permission, function () use ($userId) { return $userId === user()->id; @@ -111,7 +110,7 @@ abstract class Controller extends BaseController } /** - * Send back a json error message. + * Send back a JSON error message. */ protected function jsonError(string $messageText = '', int $statusCode = 500): JsonResponse { @@ -127,7 +126,7 @@ abstract class Controller extends BaseController } /** - * Show a positive, successful notification to the user on next view load. + * Show a positive, successful notification to the user on the next view load. */ protected function showSuccessNotification(string $message): void { @@ -135,7 +134,7 @@ abstract class Controller extends BaseController } /** - * Show a warning notification to the user on next view load. + * Show a warning notification to the user on the next view load. */ protected function showWarningNotification(string $message): void { @@ -143,7 +142,7 @@ abstract class Controller extends BaseController } /** - * Show an error notification to the user on next view load. + * Show an error notification to the user on the next view load. */ protected function showErrorNotification(string $message): void { diff --git a/app/Users/Controllers/RoleApiController.php b/app/Users/Controllers/RoleApiController.php index 2f3638cd3..6d87c9e27 100644 --- a/app/Users/Controllers/RoleApiController.php +++ b/app/Users/Controllers/RoleApiController.php @@ -10,8 +10,6 @@ use Illuminate\Support\Facades\DB; class RoleApiController extends ApiController { - protected PermissionsRepo $permissionsRepo; - protected array $fieldsToExpose = [ 'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at', ]; @@ -35,10 +33,9 @@ class RoleApiController extends ApiController ] ]; - public function __construct(PermissionsRepo $permissionsRepo) - { - $this->permissionsRepo = $permissionsRepo; - + public function __construct( + protected PermissionsRepo $permissionsRepo + ) { // Checks for all endpoints in this controller $this->middleware(function ($request, $next) { $this->checkPermission('user-roles-manage'); @@ -125,9 +122,9 @@ class RoleApiController extends ApiController } /** - * Format the given role model for single-result display. + * Format the given role model for a single-result display. */ - protected function singleFormatter(Role $role) + protected function singleFormatter(Role $role): void { $role->load('users:id,name,slug'); $role->unsetRelation('permissions'); diff --git a/database/migrations/2025_09_02_111542_remove_unused_columns.php b/database/migrations/2025_09_02_111542_remove_unused_columns.php index 1ec2dfdf9..3a36e5ad6 100644 --- a/database/migrations/2025_09_02_111542_remove_unused_columns.php +++ b/database/migrations/2025_09_02_111542_remove_unused_columns.php @@ -29,5 +29,10 @@ return new class extends Migration Schema::table('comments', function (Blueprint $table) { $table->longText('text')->nullable(); }); + + Schema::table('role_permissions', function (Blueprint $table) { + $table->string('display_name')->nullable(); + $table->string('description')->nullable(); + }); } }; From 33a0237f875170e4750ea537604307dd9d5a0bde Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 8 Sep 2025 18:14:38 +0100 Subject: [PATCH 028/139] Permissions: Updated usage of controller methods to use enum --- app/Access/LoginService.php | 3 +- .../Controllers/AuditLogApiController.php | 5 +- .../Controllers/AuditLogController.php | 5 +- .../Controllers/CommentController.php | 17 ++--- app/Activity/Controllers/WatchController.php | 3 +- .../Handlers/BaseNotificationHandler.php | 3 +- app/Activity/Tools/UserEntityWatchOptions.php | 3 +- app/Api/ApiTokenGuard.php | 3 +- app/Api/UserApiTokenController.php | 13 ++-- app/App/helpers.php | 2 +- .../Controllers/BookApiController.php | 7 +- app/Entities/Controllers/BookController.php | 31 ++++----- .../Controllers/BookshelfApiController.php | 7 +- .../Controllers/BookshelfController.php | 15 +++-- .../Controllers/ChapterApiController.php | 9 +-- .../Controllers/ChapterController.php | 32 +++++---- .../Controllers/PageApiController.php | 9 +-- app/Entities/Controllers/PageController.php | 38 +++++------ .../Controllers/PageRevisionController.php | 5 +- .../Controllers/RecycleBinApiController.php | 5 +- .../Controllers/RecycleBinController.php | 5 +- app/Entities/Tools/PageContent.php | 3 +- app/Http/Middleware/ApiAuthenticate.php | 3 +- .../Middleware/CheckUserHasPermission.php | 7 +- .../ContentPermissionApiController.php | 4 +- app/Permissions/Permission.php | 66 +++++++------------ app/Permissions/PermissionsController.php | 20 +++--- app/Settings/MaintenanceController.php | 9 +-- app/Settings/SettingController.php | 5 +- app/Sorting/BookSortController.php | 5 +- .../Controllers/AttachmentApiController.php | 13 ++-- .../Controllers/AttachmentController.php | 32 ++++----- .../Controllers/DrawioImageController.php | 3 +- .../Controllers/GalleryImageController.php | 3 +- app/Uploads/Controllers/ImageController.php | 11 ++-- .../Controllers/ImageGalleryApiController.php | 11 ++-- app/Users/Controllers/RoleApiController.php | 3 +- app/Users/Controllers/RoleController.php | 15 +++-- .../Controllers/UserAccountController.php | 5 +- app/Users/Controllers/UserApiController.php | 4 +- app/Users/Controllers/UserController.php | 15 +++-- resources/views/home/books.blade.php | 2 +- resources/views/home/shelves.blade.php | 2 +- 43 files changed, 235 insertions(+), 226 deletions(-) diff --git a/app/Access/LoginService.php b/app/Access/LoginService.php index 76ae1938d..c81e95572 100644 --- a/app/Access/LoginService.php +++ b/app/Access/LoginService.php @@ -9,6 +9,7 @@ use BookStack\Exceptions\LoginAttemptInvalidUserException; use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Facades\Activity; use BookStack\Facades\Theme; +use BookStack\Permissions\Permission; use BookStack\Theming\ThemeEvents; use BookStack\Users\Models\User; use Exception; @@ -50,7 +51,7 @@ class LoginService Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user); // Authenticate on all session guards if a likely admin - if ($user->can('users-manage') && $user->can('user-roles-manage')) { + if ($user->can(Permission::UsersManage) && $user->can(Permission::UserRolesManage)) { $guards = ['standard', 'ldap', 'saml2', 'oidc']; foreach ($guards as $guard) { auth($guard)->login($user); diff --git a/app/Activity/Controllers/AuditLogApiController.php b/app/Activity/Controllers/AuditLogApiController.php index 650d17446..0cb4d9cb6 100644 --- a/app/Activity/Controllers/AuditLogApiController.php +++ b/app/Activity/Controllers/AuditLogApiController.php @@ -4,6 +4,7 @@ namespace BookStack\Activity\Controllers; use BookStack\Activity\Models\Activity; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; class AuditLogApiController extends ApiController { @@ -16,8 +17,8 @@ class AuditLogApiController extends ApiController */ public function list() { - $this->checkPermission('settings-manage'); - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::SettingsManage); + $this->checkPermission(Permission::UsersManage); $query = Activity::query()->with(['user']); diff --git a/app/Activity/Controllers/AuditLogController.php b/app/Activity/Controllers/AuditLogController.php index 66ca30197..c4f9b91ed 100644 --- a/app/Activity/Controllers/AuditLogController.php +++ b/app/Activity/Controllers/AuditLogController.php @@ -5,6 +5,7 @@ namespace BookStack\Activity\Controllers; use BookStack\Activity\ActivityType; use BookStack\Activity\Models\Activity; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Sorting\SortUrl; use BookStack\Util\SimpleListOptions; use Illuminate\Http\Request; @@ -13,8 +14,8 @@ class AuditLogController extends Controller { public function index(Request $request) { - $this->checkPermission('settings-manage'); - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::SettingsManage); + $this->checkPermission(Permission::UsersManage); $sort = $request->get('sort', 'activity_date'); $order = $request->get('order', 'desc'); diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 479d57c4d..fd5463dff 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -7,6 +7,7 @@ use BookStack\Activity\Tools\CommentTree; use BookStack\Activity\Tools\CommentTreeNode; use BookStack\Entities\Queries\PageQueries; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -42,7 +43,7 @@ class CommentController extends Controller } // Create a new comment. - $this->checkPermission('comment-create-all'); + $this->checkPermission(Permission::CommentCreateAll); $contentRef = $input['content_ref'] ?? ''; $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef); @@ -64,8 +65,8 @@ class CommentController extends Controller ]); $comment = $this->commentRepo->getById($commentId); - $this->checkOwnablePermission('page-view', $comment->entity); - $this->checkOwnablePermission('comment-update', $comment); + $this->checkOwnablePermission(Permission::PageView, $comment->entity); + $this->checkOwnablePermission(Permission::CommentUpdate, $comment); $comment = $this->commentRepo->update($comment, $input['html']); @@ -81,8 +82,8 @@ class CommentController extends Controller public function archive(int $id) { $comment = $this->commentRepo->getById($id); - $this->checkOwnablePermission('page-view', $comment->entity); - if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { + $this->checkOwnablePermission(Permission::PageView, $comment->entity); + if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) { $this->showPermissionError(); } @@ -101,8 +102,8 @@ class CommentController extends Controller public function unarchive(int $id) { $comment = $this->commentRepo->getById($id); - $this->checkOwnablePermission('page-view', $comment->entity); - if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { + $this->checkOwnablePermission(Permission::PageView, $comment->entity); + if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) { $this->showPermissionError(); } @@ -121,7 +122,7 @@ class CommentController extends Controller public function destroy(int $id) { $comment = $this->commentRepo->getById($id); - $this->checkOwnablePermission('comment-delete', $comment); + $this->checkOwnablePermission(Permission::CommentDelete, $comment); $this->commentRepo->delete($comment); diff --git a/app/Activity/Controllers/WatchController.php b/app/Activity/Controllers/WatchController.php index 5df75da39..b77a893ea 100644 --- a/app/Activity/Controllers/WatchController.php +++ b/app/Activity/Controllers/WatchController.php @@ -5,13 +5,14 @@ namespace BookStack\Activity\Controllers; use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Tools\MixedEntityRequestHelper; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use Illuminate\Http\Request; class WatchController extends Controller { public function update(Request $request, MixedEntityRequestHelper $entityHelper) { - $this->checkPermission('receive-notifications'); + $this->checkPermission(Permission::ReceiveNotifications); $this->preventGuestAccess(); $requestData = $this->validate($request, array_merge([ diff --git a/app/Activity/Notifications/Handlers/BaseNotificationHandler.php b/app/Activity/Notifications/Handlers/BaseNotificationHandler.php index 3a9b0c1dc..bc6f2e22f 100644 --- a/app/Activity/Notifications/Handlers/BaseNotificationHandler.php +++ b/app/Activity/Notifications/Handlers/BaseNotificationHandler.php @@ -5,6 +5,7 @@ namespace BookStack\Activity\Notifications\Handlers; use BookStack\Activity\Models\Loggable; use BookStack\Activity\Notifications\Messages\BaseActivityNotification; use BookStack\Entities\Models\Entity; +use BookStack\Permissions\Permission; use BookStack\Permissions\PermissionApplicator; use BookStack\Users\Models\User; use Illuminate\Support\Facades\Log; @@ -26,7 +27,7 @@ abstract class BaseNotificationHandler implements NotificationHandler } // Prevent sending of the user does not have notification permissions - if (!$user->can('receive-notifications')) { + if (!$user->can(Permission::ReceiveNotifications)) { continue; } diff --git a/app/Activity/Tools/UserEntityWatchOptions.php b/app/Activity/Tools/UserEntityWatchOptions.php index 559d7903d..8e5f70758 100644 --- a/app/Activity/Tools/UserEntityWatchOptions.php +++ b/app/Activity/Tools/UserEntityWatchOptions.php @@ -7,6 +7,7 @@ use BookStack\Activity\WatchLevels; use BookStack\Entities\Models\BookChild; use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Page; +use BookStack\Permissions\Permission; use BookStack\Users\Models\User; use Illuminate\Database\Eloquent\Builder; @@ -22,7 +23,7 @@ class UserEntityWatchOptions public function canWatch(): bool { - return $this->user->can('receive-notifications') && !$this->user->isGuest(); + return $this->user->can(Permission::ReceiveNotifications) && !$this->user->isGuest(); } public function getWatchLevel(): string diff --git a/app/Api/ApiTokenGuard.php b/app/Api/ApiTokenGuard.php index 6302884a9..9f4537b29 100644 --- a/app/Api/ApiTokenGuard.php +++ b/app/Api/ApiTokenGuard.php @@ -4,6 +4,7 @@ namespace BookStack\Api; use BookStack\Access\LoginService; use BookStack\Exceptions\ApiAuthException; +use BookStack\Permissions\Permission; use Illuminate\Auth\GuardHelpers; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\Guard; @@ -146,7 +147,7 @@ class ApiTokenGuard implements Guard throw new ApiAuthException(trans('errors.api_user_token_expired'), 403); } - if (!$token->user->can('access-api')) { + if (!$token->user->can(Permission::AccessApi)) { throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403); } } diff --git a/app/Api/UserApiTokenController.php b/app/Api/UserApiTokenController.php index 3606e8260..2ca9e2235 100644 --- a/app/Api/UserApiTokenController.php +++ b/app/Api/UserApiTokenController.php @@ -4,6 +4,7 @@ namespace BookStack\Api; use BookStack\Activity\ActivityType; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Users\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -16,8 +17,8 @@ class UserApiTokenController extends Controller */ public function create(Request $request, int $userId) { - $this->checkPermission('access-api'); - $this->checkPermissionOrCurrentUser('users-manage', $userId); + $this->checkPermission(Permission::AccessApi); + $this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId); $this->updateContext($request); $user = User::query()->findOrFail($userId); @@ -35,8 +36,8 @@ class UserApiTokenController extends Controller */ public function store(Request $request, int $userId) { - $this->checkPermission('access-api'); - $this->checkPermissionOrCurrentUser('users-manage', $userId); + $this->checkPermission(Permission::AccessApi); + $this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId); $this->validate($request, [ 'name' => ['required', 'max:250'], @@ -143,8 +144,8 @@ class UserApiTokenController extends Controller */ protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array { - $this->checkPermissionOr('users-manage', function () use ($userId) { - return $userId === user()->id && userCan('access-api'); + $this->checkPermissionOr(Permission::UsersManage, function () use ($userId) { + return $userId === user()->id && userCan(Permission::AccessApi); }); $user = User::query()->findOrFail($userId); diff --git a/app/App/helpers.php b/app/App/helpers.php index 0e26d9974..0e357e36a 100644 --- a/app/App/helpers.php +++ b/app/App/helpers.php @@ -56,7 +56,7 @@ function userCan(string|Permission $permission, ?Model $ownable = null): bool * Check if the current user can perform the given action on any items in the system. * Can be provided the class name of an entity to filter ability to that specific entity type. */ -function userCanOnAny(string $action, string $entityClass = ''): bool +function userCanOnAny(string|Permission $action, string $entityClass = ''): bool { $permissions = app()->make(PermissionApplicator::class); diff --git a/app/Entities/Controllers/BookApiController.php b/app/Entities/Controllers/BookApiController.php index a617ee2da..5baea163f 100644 --- a/app/Entities/Controllers/BookApiController.php +++ b/app/Entities/Controllers/BookApiController.php @@ -11,6 +11,7 @@ use BookStack\Entities\Queries\PageQueries; use BookStack\Entities\Repos\BookRepo; use BookStack\Entities\Tools\BookContents; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -47,7 +48,7 @@ class BookApiController extends ApiController */ public function create(Request $request) { - $this->checkPermission('book-create-all'); + $this->checkPermission(Permission::BookCreateAll); $requestData = $this->validate($request, $this->rules()['create']); $book = $this->bookRepo->create($requestData); @@ -92,7 +93,7 @@ class BookApiController extends ApiController public function update(Request $request, string $id) { $book = $this->queries->findVisibleByIdOrFail(intval($id)); - $this->checkOwnablePermission('book-update', $book); + $this->checkOwnablePermission(Permission::BookUpdate, $book); $requestData = $this->validate($request, $this->rules()['update']); $book = $this->bookRepo->update($book, $requestData); @@ -109,7 +110,7 @@ class BookApiController extends ApiController public function delete(string $id) { $book = $this->queries->findVisibleByIdOrFail(intval($id)); - $this->checkOwnablePermission('book-delete', $book); + $this->checkOwnablePermission(Permission::BookDelete, $book); $this->bookRepo->destroy($book); diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index 5d3d67f64..cbf7ffb79 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -17,6 +17,7 @@ use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\NotFoundException; use BookStack\Facades\Activity; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\References\ReferenceFetcher; use BookStack\Util\DatabaseTransaction; use BookStack\Util\SimpleListOptions; @@ -73,12 +74,12 @@ class BookController extends Controller */ public function create(?string $shelfSlug = null) { - $this->checkPermission('book-create-all'); + $this->checkPermission(Permission::BookCreateAll); $bookshelf = null; if ($shelfSlug !== null) { $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug); - $this->checkOwnablePermission('bookshelf-update', $bookshelf); + $this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf); } $this->setPageTitle(trans('entities.books_create')); @@ -96,7 +97,7 @@ class BookController extends Controller */ public function store(Request $request, ?string $shelfSlug = null) { - $this->checkPermission('book-create-all'); + $this->checkPermission(Permission::BookCreateAll); $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], 'description_html' => ['string', 'max:2000'], @@ -108,7 +109,7 @@ class BookController extends Controller $bookshelf = null; if ($shelfSlug !== null) { $bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug); - $this->checkOwnablePermission('bookshelf-update', $bookshelf); + $this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf); } $book = $this->bookRepo->create($validated); @@ -154,7 +155,7 @@ class BookController extends Controller public function edit(string $slug) { $book = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('book-update', $book); + $this->checkOwnablePermission(Permission::BookUpdate, $book); $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()])); return view('books.edit', ['book' => $book, 'current' => $book]); @@ -170,7 +171,7 @@ class BookController extends Controller public function update(Request $request, string $slug) { $book = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('book-update', $book); + $this->checkOwnablePermission(Permission::BookUpdate, $book); $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], @@ -197,7 +198,7 @@ class BookController extends Controller public function showDelete(string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-delete', $book); + $this->checkOwnablePermission(Permission::BookDelete, $book); $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()])); return view('books.delete', ['book' => $book, 'current' => $book]); @@ -211,7 +212,7 @@ class BookController extends Controller public function destroy(string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-delete', $book); + $this->checkOwnablePermission(Permission::BookDelete, $book); $this->bookRepo->destroy($book); @@ -226,7 +227,7 @@ class BookController extends Controller public function showCopy(string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-view', $book); + $this->checkOwnablePermission(Permission::BookView, $book); session()->flashInput(['name' => $book->name]); @@ -243,8 +244,8 @@ class BookController extends Controller public function copy(Request $request, Cloner $cloner, string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-view', $book); - $this->checkPermission('book-create-all'); + $this->checkOwnablePermission(Permission::BookView, $book); + $this->checkPermission(Permission::BookCreateAll); $newName = $request->get('name') ?: $book->name; $bookCopy = $cloner->cloneBook($book, $newName); @@ -259,10 +260,10 @@ class BookController extends Controller public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-update', $book); - $this->checkOwnablePermission('book-delete', $book); - $this->checkPermission('bookshelf-create-all'); - $this->checkPermission('book-create-all'); + $this->checkOwnablePermission(Permission::BookUpdate, $book); + $this->checkOwnablePermission(Permission::BookDelete, $book); + $this->checkPermission(Permission::BookshelfCreateAll); + $this->checkPermission(Permission::BookCreateAll); $shelf = (new DatabaseTransaction(function () use ($book, $transformer) { return $transformer->transformBookToShelf($book); diff --git a/app/Entities/Controllers/BookshelfApiController.php b/app/Entities/Controllers/BookshelfApiController.php index b512f2d05..f4bd394a9 100644 --- a/app/Entities/Controllers/BookshelfApiController.php +++ b/app/Entities/Controllers/BookshelfApiController.php @@ -6,6 +6,7 @@ use BookStack\Entities\Models\Bookshelf; use BookStack\Entities\Queries\BookshelfQueries; use BookStack\Entities\Repos\BookshelfRepo; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use Exception; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Http\Request; @@ -45,7 +46,7 @@ class BookshelfApiController extends ApiController */ public function create(Request $request) { - $this->checkPermission('bookshelf-create-all'); + $this->checkPermission(Permission::BookshelfCreateAll); $requestData = $this->validate($request, $this->rules()['create']); $bookIds = $request->get('books', []); @@ -84,7 +85,7 @@ class BookshelfApiController extends ApiController public function update(Request $request, string $id) { $shelf = $this->queries->findVisibleByIdOrFail(intval($id)); - $this->checkOwnablePermission('bookshelf-update', $shelf); + $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf); $requestData = $this->validate($request, $this->rules()['update']); $bookIds = $request->get('books', null); @@ -103,7 +104,7 @@ class BookshelfApiController extends ApiController public function delete(string $id) { $shelf = $this->queries->findVisibleByIdOrFail(intval($id)); - $this->checkOwnablePermission('bookshelf-delete', $shelf); + $this->checkOwnablePermission(Permission::BookshelfDelete, $shelf); $this->bookshelfRepo->destroy($shelf); diff --git a/app/Entities/Controllers/BookshelfController.php b/app/Entities/Controllers/BookshelfController.php index 6cedd23e7..f47742ffa 100644 --- a/app/Entities/Controllers/BookshelfController.php +++ b/app/Entities/Controllers/BookshelfController.php @@ -11,6 +11,7 @@ use BookStack\Entities\Tools\ShelfContext; use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\NotFoundException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\References\ReferenceFetcher; use BookStack\Util\SimpleListOptions; use Exception; @@ -68,7 +69,7 @@ class BookshelfController extends Controller */ public function create() { - $this->checkPermission('bookshelf-create-all'); + $this->checkPermission(Permission::BookshelfCreateAll); $books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']); $this->setPageTitle(trans('entities.shelves_create')); @@ -83,7 +84,7 @@ class BookshelfController extends Controller */ public function store(Request $request) { - $this->checkPermission('bookshelf-create-all'); + $this->checkPermission(Permission::BookshelfCreateAll); $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], 'description_html' => ['string', 'max:2000'], @@ -105,7 +106,7 @@ class BookshelfController extends Controller public function show(Request $request, ActivityQueries $activities, string $slug) { $shelf = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('bookshelf-view', $shelf); + $this->checkOwnablePermission(Permission::BookshelfView, $shelf); $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([ 'default' => trans('common.sort_default'), @@ -143,7 +144,7 @@ class BookshelfController extends Controller public function edit(string $slug) { $shelf = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('bookshelf-update', $shelf); + $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf); $shelfBookIds = $shelf->books()->get(['id'])->pluck('id'); $books = $this->bookQueries->visibleForList() @@ -169,7 +170,7 @@ class BookshelfController extends Controller public function update(Request $request, string $slug) { $shelf = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('bookshelf-update', $shelf); + $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf); $validated = $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], 'description_html' => ['string', 'max:2000'], @@ -195,7 +196,7 @@ class BookshelfController extends Controller public function showDelete(string $slug) { $shelf = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('bookshelf-delete', $shelf); + $this->checkOwnablePermission(Permission::BookshelfDelete, $shelf); $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()])); @@ -210,7 +211,7 @@ class BookshelfController extends Controller public function destroy(string $slug) { $shelf = $this->queries->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('bookshelf-delete', $shelf); + $this->checkOwnablePermission(Permission::BookshelfDelete, $shelf); $this->shelfRepo->destroy($shelf); diff --git a/app/Entities/Controllers/ChapterApiController.php b/app/Entities/Controllers/ChapterApiController.php index 6ba2e9fd2..80eab7bb8 100644 --- a/app/Entities/Controllers/ChapterApiController.php +++ b/app/Entities/Controllers/ChapterApiController.php @@ -9,6 +9,7 @@ use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Repos\ChapterRepo; use BookStack\Exceptions\PermissionsException; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use Exception; use Illuminate\Http\Request; @@ -65,7 +66,7 @@ class ChapterApiController extends ApiController $bookId = $request->get('book_id'); $book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId)); - $this->checkOwnablePermission('chapter-create', $book); + $this->checkOwnablePermission(Permission::ChapterCreate, $book); $chapter = $this->chapterRepo->create($requestData, $book); @@ -101,10 +102,10 @@ class ChapterApiController extends ApiController { $requestData = $this->validate($request, $this->rules()['update']); $chapter = $this->queries->findVisibleByIdOrFail(intval($id)); - $this->checkOwnablePermission('chapter-update', $chapter); + $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) { - $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); try { $this->chapterRepo->move($chapter, "book:{$requestData['book_id']}"); @@ -129,7 +130,7 @@ class ChapterApiController extends ApiController public function delete(string $id) { $chapter = $this->queries->findVisibleByIdOrFail(intval($id)); - $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); $this->chapterRepo->destroy($chapter); diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index 677745500..9335e0a70 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -17,6 +17,7 @@ use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\PermissionsException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\References\ReferenceFetcher; use BookStack\Util\DatabaseTransaction; use Illuminate\Http\Request; @@ -39,7 +40,7 @@ class ChapterController extends Controller public function create(string $bookSlug) { $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('chapter-create', $book); + $this->checkOwnablePermission(Permission::ChapterCreate, $book); $this->setPageTitle(trans('entities.chapters_create')); @@ -64,7 +65,7 @@ class ChapterController extends Controller ]); $book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('chapter-create', $book); + $this->checkOwnablePermission(Permission::ChapterCreate, $book); $chapter = $this->chapterRepo->create($validated, $book); @@ -77,7 +78,6 @@ class ChapterController extends Controller public function show(string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-view', $chapter); $sidebarTree = (new BookContents($chapter->book))->getTree(); $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get(); @@ -106,7 +106,7 @@ class ChapterController extends Controller public function edit(string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-update', $chapter); + $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); $this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()])); @@ -128,7 +128,7 @@ class ChapterController extends Controller ]); $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-update', $chapter); + $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); $this->chapterRepo->update($chapter, $validated); @@ -143,7 +143,7 @@ class ChapterController extends Controller public function showDelete(string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); $this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()])); @@ -159,7 +159,7 @@ class ChapterController extends Controller public function destroy(string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); $this->chapterRepo->destroy($chapter); @@ -175,8 +175,8 @@ class ChapterController extends Controller { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()])); - $this->checkOwnablePermission('chapter-update', $chapter); - $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); return view('chapters.move', [ 'chapter' => $chapter, @@ -192,8 +192,8 @@ class ChapterController extends Controller public function move(Request $request, string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-update', $chapter); - $this->checkOwnablePermission('chapter-delete', $chapter); + $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); $entitySelection = $request->get('entity_selection', null); if ($entitySelection === null || $entitySelection === '') { @@ -221,7 +221,6 @@ class ChapterController extends Controller public function showCopy(string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-view', $chapter); session()->flashInput(['name' => $chapter->name]); @@ -240,7 +239,6 @@ class ChapterController extends Controller public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-view', $chapter); $entitySelection = $request->get('entity_selection') ?: null; $newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent(); @@ -251,7 +249,7 @@ class ChapterController extends Controller return redirect($chapter->getUrl('/copy')); } - $this->checkOwnablePermission('chapter-create', $newParentBook); + $this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook); $newName = $request->get('name') ?: $chapter->name; $chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName); @@ -266,9 +264,9 @@ class ChapterController extends Controller public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug) { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('chapter-update', $chapter); - $this->checkOwnablePermission('chapter-delete', $chapter); - $this->checkPermission('book-create-all'); + $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); + $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); + $this->checkPermission(Permission::BookCreateAll); $book = (new DatabaseTransaction(function () use ($chapter, $transformer) { return $transformer->transformChapterToBook($chapter); diff --git a/app/Entities/Controllers/PageApiController.php b/app/Entities/Controllers/PageApiController.php index 8fcba3dc6..033c19a7a 100644 --- a/app/Entities/Controllers/PageApiController.php +++ b/app/Entities/Controllers/PageApiController.php @@ -7,6 +7,7 @@ use BookStack\Entities\Queries\PageQueries; use BookStack\Entities\Repos\PageRepo; use BookStack\Exceptions\PermissionsException; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use Exception; use Illuminate\Http\Request; @@ -76,7 +77,7 @@ class PageApiController extends ApiController } else { $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id'))); } - $this->checkOwnablePermission('page-create', $parent); + $this->checkOwnablePermission(Permission::PageCreate, $parent); $draft = $this->pageRepo->getNewDraftPage($parent); $this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create']))); @@ -116,7 +117,7 @@ class PageApiController extends ApiController $requestData = $this->validate($request, $this->rules['update']); $page = $this->queries->findVisibleByIdOrFail($id); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $parent = null; if ($request->has('chapter_id')) { @@ -126,7 +127,7 @@ class PageApiController extends ApiController } if ($parent && !$parent->matches($page->getParent())) { - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); try { $this->pageRepo->move($page, $parent->getType() . ':' . $parent->id); @@ -151,7 +152,7 @@ class PageApiController extends ApiController public function delete(string $id) { $page = $this->queries->findVisibleByIdOrFail($id); - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); $this->pageRepo->destroy($page); diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index de3aed7d9..d4bde300b 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -20,6 +20,7 @@ use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotifyException; use BookStack\Exceptions\PermissionsException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\References\ReferenceFetcher; use Exception; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -50,7 +51,7 @@ class PageController extends Controller $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug); } - $this->checkOwnablePermission('page-create', $parent); + $this->checkOwnablePermission(Permission::PageCreate, $parent); // Redirect to draft edit screen if signed in if ($this->isSignedIn()) { @@ -82,7 +83,7 @@ class PageController extends Controller $parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug); } - $this->checkOwnablePermission('page-create', $parent); + $this->checkOwnablePermission(Permission::PageCreate, $parent); $page = $this->pageRepo->getNewDraftPage($parent); $this->pageRepo->publishDraft($page, [ @@ -100,7 +101,7 @@ class PageController extends Controller public function editDraft(Request $request, string $bookSlug, int $pageId) { $draft = $this->queries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-create', $draft->getParent()); + $this->checkOwnablePermission(Permission::PageCreate, $draft->getParent()); $editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', '')); $this->setPageTitle(trans('entities.pages_edit_draft')); @@ -120,7 +121,7 @@ class PageController extends Controller 'name' => ['required', 'string', 'max:255'], ]); $draftPage = $this->queries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-create', $draftPage->getParent()); + $this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent()); $page = $this->pageRepo->publishDraft($draftPage, $request->all()); @@ -148,8 +149,6 @@ class PageController extends Controller return redirect($page->getUrl()); } - $this->checkOwnablePermission('page-view', $page); - $pageContent = (new PageContent($page)); $page->html = $pageContent->render(); $pageNav = $pageContent->getNavigation($page->html); @@ -197,7 +196,7 @@ class PageController extends Controller public function edit(Request $request, string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-update', $page, $page->getUrl()); + $this->checkOwnablePermission(Permission::PageUpdate, $page, $page->getUrl()); $editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', '')); if ($editorData->getWarnings()) { @@ -221,7 +220,7 @@ class PageController extends Controller 'name' => ['required', 'string', 'max:255'], ]); $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $this->pageRepo->update($page, $request->all()); @@ -236,7 +235,7 @@ class PageController extends Controller public function saveDraft(Request $request, int $pageId) { $page = $this->queries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); if (!$this->isSignedIn()) { return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500); @@ -273,7 +272,7 @@ class PageController extends Controller public function showDelete(string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()])); $usedAsTemplate = $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 || @@ -295,7 +294,7 @@ class PageController extends Controller public function showDeleteDraft(string $bookSlug, int $pageId) { $page = $this->queries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()])); $usedAsTemplate = $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 || @@ -318,7 +317,7 @@ class PageController extends Controller public function destroy(string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); $parent = $page->getParent(); $this->pageRepo->destroy($page); @@ -337,7 +336,7 @@ class PageController extends Controller $page = $this->queries->findVisibleByIdOrFail($pageId); $book = $page->book; $chapter = $page->chapter; - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $this->pageRepo->destroy($page); @@ -384,8 +383,8 @@ class PageController extends Controller public function showMove(string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-update', $page); - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); return view('pages.move', [ 'book' => $page->book, @@ -402,8 +401,8 @@ class PageController extends Controller public function move(Request $request, string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-update', $page); - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); $entitySelection = $request->get('entity_selection', null); if ($entitySelection === null || $entitySelection === '') { @@ -431,7 +430,6 @@ class PageController extends Controller public function showCopy(string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-view', $page); session()->flashInput(['name' => $page->name]); return view('pages.copy', [ @@ -449,7 +447,7 @@ class PageController extends Controller public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug) { $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-view', $page); + $this->checkOwnablePermission(Permission::PageView, $page); $entitySelection = $request->get('entity_selection') ?: null; $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent(); @@ -460,7 +458,7 @@ class PageController extends Controller return redirect($page->getUrl('/copy')); } - $this->checkOwnablePermission('page-create', $newParent); + $this->checkOwnablePermission(Permission::PageCreate, $newParent); $newName = $request->get('name') ?: $page->name; $pageCopy = $cloner->clonePage($page, $newParent, $newName); diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index c43eea08b..35f1e8daf 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -11,6 +11,7 @@ use BookStack\Entities\Tools\PageContent; use BookStack\Exceptions\NotFoundException; use BookStack\Facades\Activity; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Util\SimpleListOptions; use Illuminate\Http\Request; use Ssddanbrown\HtmlDiff\Diff; @@ -124,7 +125,7 @@ class PageRevisionController extends Controller public function restore(string $bookSlug, string $pageSlug, int $revisionId) { $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $page = $this->pageRepo->restoreRevision($page, $revisionId); @@ -139,7 +140,7 @@ class PageRevisionController extends Controller public function destroy(string $bookSlug, string $pageSlug, int $revId) { $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('page-delete', $page); + $this->checkOwnablePermission(Permission::PageDelete, $page); $revision = $page->revisions()->where('id', '=', $revId)->first(); if ($revision === null) { diff --git a/app/Entities/Controllers/RecycleBinApiController.php b/app/Entities/Controllers/RecycleBinApiController.php index 89bd3f41a..614685136 100644 --- a/app/Entities/Controllers/RecycleBinApiController.php +++ b/app/Entities/Controllers/RecycleBinApiController.php @@ -9,6 +9,7 @@ use BookStack\Entities\Models\Deletion; use BookStack\Entities\Models\Page; use BookStack\Entities\Repos\DeletionRepo; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -17,8 +18,8 @@ class RecycleBinApiController extends ApiController public function __construct() { $this->middleware(function ($request, $next) { - $this->checkPermission('settings-manage'); - $this->checkPermission('restrictions-manage-all'); + $this->checkPermission(Permission::SettingsManage); + $this->checkPermission(Permission::RestrictionsManageAll); return $next($request); }); diff --git a/app/Entities/Controllers/RecycleBinController.php b/app/Entities/Controllers/RecycleBinController.php index d11dde4dd..f3c2b6a01 100644 --- a/app/Entities/Controllers/RecycleBinController.php +++ b/app/Entities/Controllers/RecycleBinController.php @@ -8,6 +8,7 @@ use BookStack\Entities\Models\Entity; use BookStack\Entities\Repos\DeletionRepo; use BookStack\Entities\Tools\TrashCan; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; class RecycleBinController extends Controller { @@ -20,8 +21,8 @@ class RecycleBinController extends Controller public function __construct() { $this->middleware(function ($request, $next) { - $this->checkPermission('settings-manage'); - $this->checkPermission('restrictions-manage-all'); + $this->checkPermission(Permission::SettingsManage); + $this->checkPermission(Permission::RestrictionsManageAll); return $next($request); }); diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index d2f5de65c..4b1d77db7 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -7,6 +7,7 @@ use BookStack\Entities\Queries\PageQueries; use BookStack\Entities\Tools\Markdown\MarkdownToHtml; use BookStack\Exceptions\ImageUploadException; use BookStack\Facades\Theme; +use BookStack\Permissions\Permission; use BookStack\Theming\ThemeEvents; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageService; @@ -122,7 +123,7 @@ class PageContent $imageInfo = $this->parseBase64ImageUri($uri); // Validate user has permission to create images - if (!$updater->can('image-create-all')) { + if (!$updater->can(Permission::ImageCreateAll)) { return ''; } diff --git a/app/Http/Middleware/ApiAuthenticate.php b/app/Http/Middleware/ApiAuthenticate.php index 5f3ad3168..15b5a325a 100644 --- a/app/Http/Middleware/ApiAuthenticate.php +++ b/app/Http/Middleware/ApiAuthenticate.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Middleware; use BookStack\Exceptions\ApiAuthException; +use BookStack\Permissions\Permission; use Closure; use Illuminate\Http\Request; @@ -51,7 +52,7 @@ class ApiAuthenticate */ protected function sessionUserHasApiAccess(): bool { - $hasApiPermission = user()->can('access-api'); + $hasApiPermission = user()->can(Permission::AccessApi); return $hasApiPermission && user()->hasAppAccess(); } diff --git a/app/Http/Middleware/CheckUserHasPermission.php b/app/Http/Middleware/CheckUserHasPermission.php index b5678e734..ea4ff652a 100644 --- a/app/Http/Middleware/CheckUserHasPermission.php +++ b/app/Http/Middleware/CheckUserHasPermission.php @@ -2,6 +2,7 @@ namespace BookStack\Http\Middleware; +use BookStack\Permissions\Permission; use Closure; use Illuminate\Http\Request; @@ -10,13 +11,9 @@ class CheckUserHasPermission /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @param string $permission - * * @return mixed */ - public function handle($request, Closure $next, $permission) + public function handle(Request $request, Closure $next, string|Permission $permission) { if (!user()->can($permission)) { return $this->errorResponse($request); diff --git a/app/Permissions/ContentPermissionApiController.php b/app/Permissions/ContentPermissionApiController.php index bddbc2c7d..0720cc478 100644 --- a/app/Permissions/ContentPermissionApiController.php +++ b/app/Permissions/ContentPermissionApiController.php @@ -51,7 +51,7 @@ class ContentPermissionApiController extends ApiController $entity = $this->entities->get($contentType) ->newQuery()->scopes(['visible'])->findOrFail($contentId); - $this->checkOwnablePermission('restrictions-manage', $entity); + $this->checkOwnablePermission(Permission::RestrictionsManage, $entity); return response()->json($this->formattedPermissionDataForEntity($entity)); } @@ -71,7 +71,7 @@ class ContentPermissionApiController extends ApiController $entity = $this->entities->get($contentType) ->newQuery()->scopes(['visible'])->findOrFail($contentId); - $this->checkOwnablePermission('restrictions-manage', $entity); + $this->checkOwnablePermission(Permission::RestrictionsManage, $entity); $data = $this->validate($request, $this->rules()['update']); $this->permissionsUpdater->updateFromApiRequestData($entity, $data); diff --git a/app/Permissions/Permission.php b/app/Permissions/Permission.php index 539bbe808..492ca2621 100644 --- a/app/Permissions/Permission.php +++ b/app/Permissions/Permission.php @@ -10,6 +10,9 @@ namespace BookStack\Permissions; * We use and still allow the string values in usage to allow for compatibility with scenarios where * users have customised their instance with additional permissions via the theme system. * This enum primarily exists for alignment within the codebase. + * + * Permissions with all/own suffixes may also be represented as a higher-level alias without the own/all + * suffix, which are used and assessed in the permission system logic. */ enum Permission: string { @@ -26,6 +29,7 @@ enum Permission: string case ContentImport = 'content-import'; case EditorChange = 'editor-change'; case ReceiveNotifications = 'receive-notifications'; + case RestrictionsManage = 'restrictions-manage'; case RestrictionsManageAll = 'restrictions-manage-all'; case RestrictionsManageOwn = 'restrictions-manage-own'; case SettingsManage = 'settings-manage'; @@ -33,33 +37,46 @@ enum Permission: string case UserRolesManage = 'user-roles-manage'; case UsersManage = 'users-manage'; - // Entity permissions - // Each has 'all' or 'own' in it's RolePermission, with the base non-suffix name being used - // in actual checking logic, with the permission system handling the assessment of the underlying RolePermission. + // Non-entity content permissions case AttachmentCreate = 'attachment-create'; case AttachmentCreateAll = 'attachment-create-all'; case AttachmentCreateOwn = 'attachment-create-own'; - case AttachmentDelete = 'attachment-delete'; case AttachmentDeleteAll = 'attachment-delete-all'; case AttachmentDeleteOwn = 'attachment-delete-own'; - case AttachmentUpdate = 'attachment-update'; case AttachmentUpdateAll = 'attachment-update-all'; case AttachmentUpdateOwn = 'attachment-update-own'; + case CommentCreate = 'comment-create'; + case CommentCreateAll = 'comment-create-all'; + case CommentCreateOwn = 'comment-create-own'; + case CommentDelete = 'comment-delete'; + case CommentDeleteAll = 'comment-delete-all'; + case CommentDeleteOwn = 'comment-delete-own'; + case CommentUpdate = 'comment-update'; + case CommentUpdateAll = 'comment-update-all'; + case CommentUpdateOwn = 'comment-update-own'; + + case ImageCreateAll = 'image-create-all'; + case ImageCreateOwn = 'image-create-own'; + case ImageDelete = 'image-delete'; + case ImageDeleteAll = 'image-delete-all'; + case ImageDeleteOwn = 'image-delete-own'; + case ImageUpdate = 'image-update'; + case ImageUpdateAll = 'image-update-all'; + case ImageUpdateOwn = 'image-update-own'; + + // Entity content permissions case BookCreate = 'book-create'; case BookCreateAll = 'book-create-all'; case BookCreateOwn = 'book-create-own'; - case BookDelete = 'book-delete'; case BookDeleteAll = 'book-delete-all'; case BookDeleteOwn = 'book-delete-own'; - case BookUpdate = 'book-update'; case BookUpdateAll = 'book-update-all'; case BookUpdateOwn = 'book-update-own'; - case BookView = 'book-view'; case BookViewAll = 'book-view-all'; case BookViewOwn = 'book-view-own'; @@ -67,15 +84,12 @@ enum Permission: string case BookshelfCreate = 'bookshelf-create'; case BookshelfCreateAll = 'bookshelf-create-all'; case BookshelfCreateOwn = 'bookshelf-create-own'; - case BookshelfDelete = 'bookshelf-delete'; case BookshelfDeleteAll = 'bookshelf-delete-all'; case BookshelfDeleteOwn = 'bookshelf-delete-own'; - case BookshelfUpdate = 'bookshelf-update'; case BookshelfUpdateAll = 'bookshelf-update-all'; case BookshelfUpdateOwn = 'bookshelf-update-own'; - case BookshelfView = 'bookshelf-view'; case BookshelfViewAll = 'bookshelf-view-all'; case BookshelfViewOwn = 'bookshelf-view-own'; @@ -83,55 +97,25 @@ enum Permission: string case ChapterCreate = 'chapter-create'; case ChapterCreateAll = 'chapter-create-all'; case ChapterCreateOwn = 'chapter-create-own'; - case ChapterDelete = 'chapter-delete'; case ChapterDeleteAll = 'chapter-delete-all'; case ChapterDeleteOwn = 'chapter-delete-own'; - case ChapterUpdate = 'chapter-update'; case ChapterUpdateAll = 'chapter-update-all'; case ChapterUpdateOwn = 'chapter-update-own'; - case ChapterView = 'chapter-view'; case ChapterViewAll = 'chapter-view-all'; case ChapterViewOwn = 'chapter-view-own'; - case CommentCreate = 'comment-create'; - case CommentCreateAll = 'comment-create-all'; - case CommentCreateOwn = 'comment-create-own'; - - case CommentDelete = 'comment-delete'; - case CommentDeleteAll = 'comment-delete-all'; - case CommentDeleteOwn = 'comment-delete-own'; - - case CommentUpdate = 'comment-update'; - case CommentUpdateAll = 'comment-update-all'; - case CommentUpdateOwn = 'comment-update-own'; - - case ImageCreate = 'image-create'; - case ImageCreateAll = 'image-create-all'; - case ImageCreateOwn = 'image-create-own'; - - case ImageDelete = 'image-delete'; - case ImageDeleteAll = 'image-delete-all'; - case ImageDeleteOwn = 'image-delete-own'; - - case ImageUpdate = 'image-update'; - case ImageUpdateAll = 'image-update-all'; - case ImageUpdateOwn = 'image-update-own'; - case PageCreate = 'page-create'; case PageCreateAll = 'page-create-all'; case PageCreateOwn = 'page-create-own'; - case PageDelete = 'page-delete'; case PageDeleteAll = 'page-delete-all'; case PageDeleteOwn = 'page-delete-own'; - case PageUpdate = 'page-update'; case PageUpdateAll = 'page-update-all'; case PageUpdateOwn = 'page-update-own'; - case PageView = 'page-view'; case PageViewAll = 'page-view-all'; case PageViewOwn = 'page-view-own'; diff --git a/app/Permissions/PermissionsController.php b/app/Permissions/PermissionsController.php index 9dcfe242e..d93d9e718 100644 --- a/app/Permissions/PermissionsController.php +++ b/app/Permissions/PermissionsController.php @@ -24,7 +24,7 @@ class PermissionsController extends Controller public function showForPage(string $bookSlug, string $pageSlug) { $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('restrictions-manage', $page); + $this->checkOwnablePermission(Permission::RestrictionsManage, $page); $this->setPageTitle(trans('entities.pages_permissions')); return view('pages.permissions', [ @@ -39,7 +39,7 @@ class PermissionsController extends Controller public function updateForPage(Request $request, string $bookSlug, string $pageSlug) { $page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug); - $this->checkOwnablePermission('restrictions-manage', $page); + $this->checkOwnablePermission(Permission::RestrictionsManage, $page); (new DatabaseTransaction(function () use ($page, $request) { $this->permissionsUpdater->updateFromPermissionsForm($page, $request); @@ -56,7 +56,7 @@ class PermissionsController extends Controller public function showForChapter(string $bookSlug, string $chapterSlug) { $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('restrictions-manage', $chapter); + $this->checkOwnablePermission(Permission::RestrictionsManage, $chapter); $this->setPageTitle(trans('entities.chapters_permissions')); return view('chapters.permissions', [ @@ -71,7 +71,7 @@ class PermissionsController extends Controller public function updateForChapter(Request $request, string $bookSlug, string $chapterSlug) { $chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $this->checkOwnablePermission('restrictions-manage', $chapter); + $this->checkOwnablePermission(Permission::RestrictionsManage, $chapter); (new DatabaseTransaction(function () use ($chapter, $request) { $this->permissionsUpdater->updateFromPermissionsForm($chapter, $request); @@ -88,7 +88,7 @@ class PermissionsController extends Controller public function showForBook(string $slug) { $book = $this->queries->books->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('restrictions-manage', $book); + $this->checkOwnablePermission(Permission::RestrictionsManage, $book); $this->setPageTitle(trans('entities.books_permissions')); return view('books.permissions', [ @@ -103,7 +103,7 @@ class PermissionsController extends Controller public function updateForBook(Request $request, string $slug) { $book = $this->queries->books->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('restrictions-manage', $book); + $this->checkOwnablePermission(Permission::RestrictionsManage, $book); (new DatabaseTransaction(function () use ($book, $request) { $this->permissionsUpdater->updateFromPermissionsForm($book, $request); @@ -120,7 +120,7 @@ class PermissionsController extends Controller public function showForShelf(string $slug) { $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('restrictions-manage', $shelf); + $this->checkOwnablePermission(Permission::RestrictionsManage, $shelf); $this->setPageTitle(trans('entities.shelves_permissions')); return view('shelves.permissions', [ @@ -135,7 +135,7 @@ class PermissionsController extends Controller public function updateForShelf(Request $request, string $slug) { $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('restrictions-manage', $shelf); + $this->checkOwnablePermission(Permission::RestrictionsManage, $shelf); (new DatabaseTransaction(function () use ($shelf, $request) { $this->permissionsUpdater->updateFromPermissionsForm($shelf, $request); @@ -152,7 +152,7 @@ class PermissionsController extends Controller public function copyShelfPermissionsToBooks(string $slug) { $shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug); - $this->checkOwnablePermission('restrictions-manage', $shelf); + $this->checkOwnablePermission(Permission::RestrictionsManage, $shelf); $updateCount = (new DatabaseTransaction(function () use ($shelf) { return $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf); @@ -168,7 +168,7 @@ class PermissionsController extends Controller */ public function formRowForRole(string $entityType, string $roleId) { - $this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own')); + $this->checkPermissionOr(Permission::RestrictionsManageAll, fn() => userCan(Permission::RestrictionsManageOwn)); $role = Role::query()->findOrFail($roleId); diff --git a/app/Settings/MaintenanceController.php b/app/Settings/MaintenanceController.php index ac9dd20cc..b2b2226bf 100644 --- a/app/Settings/MaintenanceController.php +++ b/app/Settings/MaintenanceController.php @@ -6,6 +6,7 @@ use BookStack\Activity\ActivityType; use BookStack\App\AppVersion; use BookStack\Entities\Tools\TrashCan; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\References\ReferenceStore; use BookStack\Uploads\ImageService; use Illuminate\Http\Request; @@ -17,7 +18,7 @@ class MaintenanceController extends Controller */ public function index(TrashCan $trashCan) { - $this->checkPermission('settings-manage'); + $this->checkPermission(Permission::SettingsManage); $this->setPageTitle(trans('settings.maint')); // Recycle bin details @@ -34,7 +35,7 @@ class MaintenanceController extends Controller */ public function cleanupImages(Request $request, ImageService $imageService) { - $this->checkPermission('settings-manage'); + $this->checkPermission(Permission::SettingsManage); $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images'); $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true'); @@ -62,7 +63,7 @@ class MaintenanceController extends Controller */ public function sendTestEmail() { - $this->checkPermission('settings-manage'); + $this->checkPermission(Permission::SettingsManage); $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email'); try { @@ -81,7 +82,7 @@ class MaintenanceController extends Controller */ public function regenerateReferences(ReferenceStore $referenceStore) { - $this->checkPermission('settings-manage'); + $this->checkPermission(Permission::SettingsManage); $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'regenerate-references'); try { diff --git a/app/Settings/SettingController.php b/app/Settings/SettingController.php index 3b7ba74d5..f89bc6dab 100644 --- a/app/Settings/SettingController.php +++ b/app/Settings/SettingController.php @@ -5,6 +5,7 @@ namespace BookStack\Settings; use BookStack\Activity\ActivityType; use BookStack\App\AppVersion; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Users\Models\User; use Illuminate\Http\Request; @@ -24,7 +25,7 @@ class SettingController extends Controller public function category(string $category) { $this->ensureCategoryExists($category); - $this->checkPermission('settings-manage'); + $this->checkPermission(Permission::SettingsManage); $this->setPageTitle(trans('settings.settings')); return view('settings.categories.' . $category, [ @@ -41,7 +42,7 @@ class SettingController extends Controller { $this->ensureCategoryExists($category); $this->preventAccessInDemoMode(); - $this->checkPermission('settings-manage'); + $this->checkPermission(Permission::SettingsManage); $this->validate($request, [ 'app_logo' => ['nullable', ...$this->getImageValidationRules()], 'app_icon' => ['nullable', ...$this->getImageValidationRules()], diff --git a/app/Sorting/BookSortController.php b/app/Sorting/BookSortController.php index d70d0e656..7e2ee5465 100644 --- a/app/Sorting/BookSortController.php +++ b/app/Sorting/BookSortController.php @@ -7,6 +7,7 @@ use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Tools\BookContents; use BookStack\Facades\Activity; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Util\DatabaseTransaction; use Illuminate\Http\Request; @@ -23,7 +24,7 @@ class BookSortController extends Controller public function show(string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-update', $book); + $this->checkOwnablePermission(Permission::BookUpdate, $book); $bookChildren = (new BookContents($book))->getTree(false); @@ -51,7 +52,7 @@ class BookSortController extends Controller public function update(Request $request, BookSorter $sorter, string $bookSlug) { $book = $this->queries->findVisibleBySlugOrFail($bookSlug); - $this->checkOwnablePermission('book-update', $book); + $this->checkOwnablePermission(Permission::BookUpdate, $book); $loggedActivityForBook = false; // Sort via map diff --git a/app/Uploads/Controllers/AttachmentApiController.php b/app/Uploads/Controllers/AttachmentApiController.php index 87e00257c..b47d6ff8d 100644 --- a/app/Uploads/Controllers/AttachmentApiController.php +++ b/app/Uploads/Controllers/AttachmentApiController.php @@ -5,6 +5,7 @@ namespace BookStack\Uploads\Controllers; use BookStack\Entities\Queries\PageQueries; use BookStack\Exceptions\FileUploadException; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use BookStack\Uploads\Attachment; use BookStack\Uploads\AttachmentService; use Exception; @@ -45,12 +46,12 @@ class AttachmentApiController extends ApiController */ public function create(Request $request) { - $this->checkPermission('attachment-create-all'); + $this->checkPermission(Permission::AttachmentCreateAll); $requestData = $this->validate($request, $this->rules()['create']); $pageId = $request->get('uploaded_to'); $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); if ($request->hasFile('file')) { $uploadedFile = $request->file('file'); @@ -137,9 +138,9 @@ class AttachmentApiController extends ApiController $attachment->uploaded_to = $requestData['uploaded_to']; } - $this->checkOwnablePermission('page-view', $page); - $this->checkOwnablePermission('page-update', $page); - $this->checkOwnablePermission('attachment-update', $attachment); + $this->checkOwnablePermission(Permission::PageView, $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); + $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); if ($request->hasFile('file')) { $uploadedFile = $request->file('file'); @@ -160,7 +161,7 @@ class AttachmentApiController extends ApiController { /** @var Attachment $attachment */ $attachment = Attachment::visible()->findOrFail($id); - $this->checkOwnablePermission('attachment-delete', $attachment); + $this->checkOwnablePermission(Permission::AttachmentDelete, $attachment); $this->attachmentService->deleteFile($attachment); diff --git a/app/Uploads/Controllers/AttachmentController.php b/app/Uploads/Controllers/AttachmentController.php index 809cdfa58..0886193e4 100644 --- a/app/Uploads/Controllers/AttachmentController.php +++ b/app/Uploads/Controllers/AttachmentController.php @@ -7,6 +7,7 @@ use BookStack\Entities\Repos\PageRepo; use BookStack\Exceptions\FileUploadException; use BookStack\Exceptions\NotFoundException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Uploads\Attachment; use BookStack\Uploads\AttachmentService; use Exception; @@ -40,8 +41,8 @@ class AttachmentController extends Controller $pageId = $request->get('uploaded_to'); $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - $this->checkPermission('attachment-create-all'); - $this->checkOwnablePermission('page-update', $page); + $this->checkPermission(Permission::AttachmentCreateAll); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $uploadedFile = $request->file('file'); @@ -67,9 +68,9 @@ class AttachmentController extends Controller /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); - $this->checkOwnablePermission('view', $attachment->page); - $this->checkOwnablePermission('page-update', $attachment->page); - $this->checkOwnablePermission('attachment-create', $attachment); + $this->checkOwnablePermission(Permission::PageView, $attachment->page); + $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page); + $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); $uploadedFile = $request->file('file'); @@ -90,8 +91,8 @@ class AttachmentController extends Controller /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); - $this->checkOwnablePermission('page-update', $attachment->page); - $this->checkOwnablePermission('attachment-create', $attachment); + $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page); + $this->checkOwnablePermission(Permission::AttachmentCreate, $attachment); return view('attachments.manager-edit-form', [ 'attachment' => $attachment, @@ -118,9 +119,9 @@ class AttachmentController extends Controller ]), 422); } - $this->checkOwnablePermission('page-view', $attachment->page); - $this->checkOwnablePermission('page-update', $attachment->page); - $this->checkOwnablePermission('attachment-update', $attachment); + $this->checkOwnablePermission(Permission::PageView, $attachment->page); + $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page); + $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); $attachment = $this->attachmentService->updateFile($attachment, [ 'name' => $request->get('attachment_edit_name'), @@ -156,8 +157,8 @@ class AttachmentController extends Controller $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - $this->checkPermission('attachment-create-all'); - $this->checkOwnablePermission('page-update', $page); + $this->checkPermission(Permission::AttachmentCreateAll); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $attachmentName = $request->get('attachment_link_name'); $link = $request->get('attachment_link_url'); @@ -176,7 +177,6 @@ class AttachmentController extends Controller public function listForPage(int $pageId) { $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-view', $page); return view('attachments.manager-list', [ 'attachments' => $page->attachments->all(), @@ -195,7 +195,7 @@ class AttachmentController extends Controller 'order' => ['required', 'array'], ]); $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $attachmentOrder = $request->get('order'); $this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId); @@ -220,7 +220,7 @@ class AttachmentController extends Controller throw new NotFoundException(trans('errors.attachment_not_found')); } - $this->checkOwnablePermission('page-view', $page); + $this->checkOwnablePermission(Permission::PageView, $page); if ($attachment->external) { return redirect($attachment->path); @@ -246,7 +246,7 @@ class AttachmentController extends Controller { /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); - $this->checkOwnablePermission('attachment-delete', $attachment); + $this->checkOwnablePermission(Permission::AttachmentDelete, $attachment); $this->attachmentService->deleteFile($attachment); return response()->json(['message' => trans('entities.attachments_deleted')]); diff --git a/app/Uploads/Controllers/DrawioImageController.php b/app/Uploads/Controllers/DrawioImageController.php index 6293da4f7..aff27b3b1 100644 --- a/app/Uploads/Controllers/DrawioImageController.php +++ b/app/Uploads/Controllers/DrawioImageController.php @@ -4,6 +4,7 @@ namespace BookStack\Uploads\Controllers; use BookStack\Exceptions\ImageUploadException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageResizer; use BookStack\Util\OutOfMemoryHandler; @@ -57,7 +58,7 @@ class DrawioImageController extends Controller 'uploaded_to' => ['required', 'integer'], ]); - $this->checkPermission('image-create-all'); + $this->checkPermission(Permission::ImageCreateAll); $imageBase64Data = $request->get('image'); try { diff --git a/app/Uploads/Controllers/GalleryImageController.php b/app/Uploads/Controllers/GalleryImageController.php index 1bc9da2d7..745efcde8 100644 --- a/app/Uploads/Controllers/GalleryImageController.php +++ b/app/Uploads/Controllers/GalleryImageController.php @@ -4,6 +4,7 @@ namespace BookStack\Uploads\Controllers; use BookStack\Exceptions\ImageUploadException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageResizer; use BookStack\Util\OutOfMemoryHandler; @@ -52,7 +53,7 @@ class GalleryImageController extends Controller */ public function create(Request $request) { - $this->checkPermission('image-create-all'); + $this->checkPermission(Permission::ImageCreateAll); try { $this->validate($request, [ diff --git a/app/Uploads/Controllers/ImageController.php b/app/Uploads/Controllers/ImageController.php index c68ffdf6b..da67639c1 100644 --- a/app/Uploads/Controllers/ImageController.php +++ b/app/Uploads/Controllers/ImageController.php @@ -6,6 +6,7 @@ use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\NotFoundException; use BookStack\Exceptions\NotifyException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Uploads\Image; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageResizer; @@ -50,7 +51,7 @@ class ImageController extends Controller $image = $this->imageRepo->getById($id); $this->checkImagePermission($image); - $this->checkOwnablePermission('image-update', $image); + $this->checkOwnablePermission(Permission::ImageUpdate, $image); $image = $this->imageRepo->updateImageDetails($image, $data); @@ -71,7 +72,7 @@ class ImageController extends Controller $image = $this->imageRepo->getById($id); $this->checkImagePermission($image); - $this->checkOwnablePermission('image-update', $image); + $this->checkOwnablePermission(Permission::ImageUpdate, $image); $file = $request->file('file'); new OutOfMemoryHandler(function () { @@ -125,7 +126,7 @@ class ImageController extends Controller public function destroy(string $id) { $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('image-delete', $image); + $this->checkOwnablePermission(Permission::ImageDelete, $image); $this->checkImagePermission($image); $this->imageRepo->destroyImage($image); @@ -140,7 +141,7 @@ class ImageController extends Controller { $image = $this->imageRepo->getById($id); $this->checkImagePermission($image); - $this->checkOwnablePermission('image-update', $image); + $this->checkOwnablePermission(Permission::ImageUpdate, $image); new OutOfMemoryHandler(function () { return $this->jsonError(trans('errors.image_thumbnail_memory_limit')); @@ -163,7 +164,7 @@ class ImageController extends Controller $relatedPage = $image->getPage(); if ($relatedPage) { - $this->checkOwnablePermission('page-view', $relatedPage); + $this->checkOwnablePermission(Permission::PageView, $relatedPage); } } } diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index d2a750c45..59696dc9a 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -4,6 +4,7 @@ namespace BookStack\Uploads\Controllers; use BookStack\Entities\Queries\PageQueries; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use BookStack\Uploads\Image; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageResizer; @@ -65,7 +66,7 @@ class ImageGalleryApiController extends ApiController */ public function create(Request $request) { - $this->checkPermission('image-create-all'); + $this->checkPermission(Permission::ImageCreateAll); $data = $this->validate($request, $this->rules()['create']); $page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']); @@ -102,8 +103,8 @@ class ImageGalleryApiController extends ApiController { $data = $this->validate($request, $this->rules()['update']); $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('page-view', $image->getPage()); - $this->checkOwnablePermission('image-update', $image); + $this->checkOwnablePermission(Permission::PageView, $image->getPage()); + $this->checkOwnablePermission(Permission::ImageUpdate, $image); $this->imageRepo->updateImageDetails($image, $data); if (isset($data['image'])) { @@ -121,8 +122,8 @@ class ImageGalleryApiController extends ApiController public function delete(string $id) { $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('page-view', $image->getPage()); - $this->checkOwnablePermission('image-delete', $image); + $this->checkOwnablePermission(Permission::PageView, $image->getPage()); + $this->checkOwnablePermission(Permission::ImageDelete, $image); $this->imageRepo->destroyImage($image); return response('', 204); diff --git a/app/Users/Controllers/RoleApiController.php b/app/Users/Controllers/RoleApiController.php index 6d87c9e27..93ecc549b 100644 --- a/app/Users/Controllers/RoleApiController.php +++ b/app/Users/Controllers/RoleApiController.php @@ -3,6 +3,7 @@ namespace BookStack\Users\Controllers; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use BookStack\Permissions\PermissionsRepo; use BookStack\Users\Models\Role; use Illuminate\Http\Request; @@ -38,7 +39,7 @@ class RoleApiController extends ApiController ) { // Checks for all endpoints in this controller $this->middleware(function ($request, $next) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); return $next($request); }); diff --git a/app/Users/Controllers/RoleController.php b/app/Users/Controllers/RoleController.php index 0a7fdcc9b..549f6e0ac 100644 --- a/app/Users/Controllers/RoleController.php +++ b/app/Users/Controllers/RoleController.php @@ -4,6 +4,7 @@ namespace BookStack\Users\Controllers; use BookStack\Exceptions\PermissionsException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Permissions\PermissionsRepo; use BookStack\Users\Models\Role; use BookStack\Users\Queries\RolesAllPaginatedAndSorted; @@ -23,7 +24,7 @@ class RoleController extends Controller */ public function index(Request $request) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); $listOptions = SimpleListOptions::fromRequest($request, 'roles')->withSortOptions([ 'display_name' => trans('common.sort_name'), @@ -49,7 +50,7 @@ class RoleController extends Controller */ public function create(Request $request) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); /** @var ?Role $role */ $role = null; @@ -71,7 +72,7 @@ class RoleController extends Controller */ public function store(Request $request) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); $data = $this->validate($request, [ 'display_name' => ['required', 'min:3', 'max:180'], 'description' => ['max:180'], @@ -92,7 +93,7 @@ class RoleController extends Controller */ public function edit(string $id) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); $role = $this->permissionsRepo->getRoleById($id); $this->setPageTitle(trans('settings.role_edit')); @@ -105,7 +106,7 @@ class RoleController extends Controller */ public function update(Request $request, string $id) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); $data = $this->validate($request, [ 'display_name' => ['required', 'min:3', 'max:180'], 'description' => ['max:180'], @@ -127,7 +128,7 @@ class RoleController extends Controller */ public function showDelete(string $id) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); $role = $this->permissionsRepo->getRoleById($id); $roles = $this->permissionsRepo->getAllRolesExcept($role); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); @@ -146,7 +147,7 @@ class RoleController extends Controller */ public function delete(Request $request, string $id) { - $this->checkPermission('user-roles-manage'); + $this->checkPermission(Permission::UserRolesManage); try { $migrateRoleId = intval($request->get('migrate_role_id') ?: "0"); diff --git a/app/Users/Controllers/UserAccountController.php b/app/Users/Controllers/UserAccountController.php index 708a91e9d..54ca69c5d 100644 --- a/app/Users/Controllers/UserAccountController.php +++ b/app/Users/Controllers/UserAccountController.php @@ -4,6 +4,7 @@ namespace BookStack\Users\Controllers; use BookStack\Access\SocialDriverManager; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Permissions\PermissionApplicator; use BookStack\Settings\UserNotificationPreferences; use BookStack\Settings\UserShortcutMap; @@ -122,7 +123,7 @@ class UserAccountController extends Controller */ public function showNotifications(PermissionApplicator $permissions) { - $this->checkPermission('receive-notifications'); + $this->checkPermission(Permission::ReceiveNotifications); $preferences = (new UserNotificationPreferences(user())); @@ -145,7 +146,7 @@ class UserAccountController extends Controller public function updateNotifications(Request $request) { $this->preventAccessInDemoMode(); - $this->checkPermission('receive-notifications'); + $this->checkPermission(Permission::ReceiveNotifications); $data = $this->validate($request, [ 'preferences' => ['required', 'array'], 'preferences.*' => ['required', 'string'], diff --git a/app/Users/Controllers/UserApiController.php b/app/Users/Controllers/UserApiController.php index 351893b03..1efc82500 100644 --- a/app/Users/Controllers/UserApiController.php +++ b/app/Users/Controllers/UserApiController.php @@ -4,9 +4,9 @@ namespace BookStack\Users\Controllers; use BookStack\Exceptions\UserUpdateException; use BookStack\Http\ApiController; +use BookStack\Permissions\Permission; use BookStack\Users\Models\User; use BookStack\Users\UserRepo; -use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rules\Password; @@ -26,7 +26,7 @@ class UserApiController extends ApiController // Checks for all endpoints in this controller $this->middleware(function ($request, $next) { - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $this->preventAccessInDemoMode(); return $next($request); diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php index c6e4326e9..494221b14 100644 --- a/app/Users/Controllers/UserController.php +++ b/app/Users/Controllers/UserController.php @@ -7,6 +7,7 @@ use BookStack\Access\UserInviteException; use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\UserUpdateException; use BookStack\Http\Controller; +use BookStack\Permissions\Permission; use BookStack\Uploads\ImageRepo; use BookStack\Users\Models\Role; use BookStack\Users\Queries\UsersAllPaginatedAndSorted; @@ -32,7 +33,7 @@ class UserController extends Controller */ public function index(Request $request) { - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $listOptions = SimpleListOptions::fromRequest($request, 'users')->withSortOptions([ 'name' => trans('common.sort_name'), @@ -58,7 +59,7 @@ class UserController extends Controller */ public function create() { - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $authMethod = config('auth.method'); $roles = Role::query()->orderBy('display_name', 'asc')->get(); $this->setPageTitle(trans('settings.users_add_new')); @@ -73,7 +74,7 @@ class UserController extends Controller */ public function store(Request $request) { - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $authMethod = config('auth.method'); $sendInvite = ($request->get('send_invite', 'false') === 'true'); @@ -111,7 +112,7 @@ class UserController extends Controller */ public function edit(int $id, SocialDriverManager $socialDriverManager) { - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $user = $this->userRepo->getById($id); $user->load(['apiTokens', 'mfaValues']); @@ -141,7 +142,7 @@ class UserController extends Controller public function update(Request $request, int $id) { $this->preventAccessInDemoMode(); - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $validated = $this->validate($request, [ 'name' => ['min:1', 'max:100'], @@ -182,7 +183,7 @@ class UserController extends Controller */ public function delete(int $id) { - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $user = $this->userRepo->getById($id); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); @@ -198,7 +199,7 @@ class UserController extends Controller public function destroy(Request $request, int $id) { $this->preventAccessInDemoMode(); - $this->checkPermission('users-manage'); + $this->checkPermission(Permission::UsersManage); $user = $this->userRepo->getById($id); $newOwnerId = intval($request->get('new_owner_id')) ?: null; diff --git a/resources/views/home/books.blade.php b/resources/views/home/books.blade.php index a2f2bf796..e0d2aeb1c 100644 --- a/resources/views/home/books.blade.php +++ b/resources/views/home/books.blade.php @@ -12,7 +12,7 @@
{{ trans('common.actions') }}