diff --git a/changelog.txt b/changelog.txt index f747f9bf2..23b34e520 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,6 @@ += 5.4.1 = +* Improved Auto-Verification compatibility with forms on query-based page URLs and Brevo forms, while simplifying form registration storage. + = 5.4.0 = * Added optional AJAX submission for standard WordPress comment forms, with inline error messages. * Added form data processing for anti-spam checks in 10 popular integrations: WordPress Core, WooCommerce, Divi, Ultimate Addons for Elementor, Essential Addons, Mailchimp, Maintenance, Ultimate Member, bbPress, and GiveWP. diff --git a/hcaptcha.php b/hcaptcha.php index bcc40149f..7801713c4 100644 --- a/hcaptcha.php +++ b/hcaptcha.php @@ -10,7 +10,7 @@ * Plugin Name: hCaptcha for WP * Plugin URI: https://www.hcaptcha.com/ * Description: hCaptcha keeps out bots and spam while putting privacy first. It is a drop-in replacement for reCAPTCHA. - * Version: 5.4.0 + * Version: 5.4.1 * Requires at least: 6.0 * Requires PHP: 7.4 * Author: hCaptcha @@ -39,7 +39,7 @@ /** * Plugin version. */ -const HCAPTCHA_VERSION = '5.4.0'; +const HCAPTCHA_VERSION = '5.4.1'; /** * Path to the plugin dir. diff --git a/package.json b/package.json index 38781e7c8..6717c7833 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,11 @@ }, "resolutions": { "@babel/plugin-transform-modules-systemjs": "^7.29.7", - "brace-expansion@npm:^1": "^1.1.16", - "brace-expansion@npm:^2": "^2.1.2", - "brace-expansion@npm:^5": "^5.0.8", + "brace-expansion@npm:^1": "^1.1.21", + "brace-expansion@npm:^2": "^2.1.7", + "brace-expansion@npm:^5": "^5.0.12", "cross-spawn": "^7.0.6", - "fast-uri": "^3.1.7", + "fast-uri": "^3.1.8", "flatted": "^3.4.2", "js-yaml": "^4.3.0", "micromatch": "^4.0.8", @@ -83,7 +83,7 @@ "picomatch@npm:^4": "^4.0.5", "postcss": "^8.5.25", "qs": "^6.15.3", - "serialize-javascript": "^7.0.6", + "serialize-javascript": "^7.1.2", "svgo": "^4.1.0", "tar": "^7.5.22", "undici": "^7.29.0" diff --git a/readme.txt b/readme.txt index c7518e748..15f6955dd 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: captcha, hcaptcha, recaptcha, antispam, spam Requires at least: 6.0 Tested up to: 7.1 Requires PHP: 7.4 -Stable tag: 5.4.0 +Stable tag: 5.4.1 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -1053,6 +1053,10 @@ Instructions for popular native integrations are below: == Changelog == += 5.4.1 = +* Fixed WordPress login authentication when login integrations have different protection settings. +* Improved Auto-Verification compatibility with forms on query-based page URLs and Brevo forms, while simplifying form registration storage. + = 5.4.0 = * Added optional AJAX submission for standard WordPress comment forms, with inline error messages. * Added form data processing for anti-spam checks in 10 popular integrations: WordPress Core, WooCommerce, Divi, Ultimate Addons for Elementor, Essential Addons, Mailchimp, Maintenance, Ultimate Member, bbPress, and GiveWP. diff --git a/src/php/Abstracts/LoginBase.php b/src/php/Abstracts/LoginBase.php index 38019b623..8441f9449 100644 --- a/src/php/Abstracts/LoginBase.php +++ b/src/php/Abstracts/LoginBase.php @@ -29,6 +29,17 @@ abstract class LoginBase { */ protected const NONCE = 'hcaptcha_login_nonce'; + /** + * Settings for signed login form contexts. + */ + private const FORM_SETTINGS = [ + 'wp-login' => 'wp_status', + 'elementor-login' => 'elementor_pro_status', + 'divi-login' => 'divi_status', + 'divi_builder-login' => 'divi_builder_status', + 'extra-login' => 'extra_status', + ]; + /** * Legacy login attempts the data option name. */ @@ -118,12 +129,15 @@ protected function init_hooks(): void { /** * Display signature. * + * @param string $form_id Signed rendering context, independent of the login threshold. + * * @return void */ - public function display_signature(): void { + public function display_signature( string $form_id = 'login' ): void { $this->login_form_shown = true; + $form_id = doing_action( 'login_form' ) && $this->is_wp_login_form() ? 'wp-login' : $form_id; - HCaptcha::display_signature( static::class, 'login', $this->hcaptcha_shown ); + HCaptcha::display_signature( static::class, $form_id, $this->hcaptcha_shown ); } /** @@ -195,8 +209,53 @@ public function allow_wp_login_skip_verification( $can_skip ): bool { * @return bool */ private function is_login_verification_owner(): bool { - return false !== has_filter( 'wp_authenticate_user', [ $this, 'check_signature' ] ) && - null === HCaptcha::check_signature( static::class, 'login' ); + [ $check, $enabled ] = $this->get_login_signature(); + + return $this->is_login_enabled() && + false !== has_filter( 'wp_authenticate_user', [ $this, 'check_signature' ] ) && + $enabled && null === $check; + } + + /** + * Validate the rendering context and look up its current protection setting. + * + * Legacy signatures still require the current threshold. Only a valid signed + * context can select a deliberately disabled form; missing or tampered fields + * cannot turn a below-threshold signature into a permanent exemption. + * + * @return array{0: bool|null, 1: bool} Signature result and current protection setting. + */ + private function get_login_signature(): array { + $check = HCaptcha::check_signature( static::class, 'login' ); + + if ( false !== $check ) { + return [ $check, true ]; + } + + foreach ( self::FORM_SETTINGS as $form_id => $setting ) { + $check = HCaptcha::check_signature( static::class, $form_id ); + + if ( false !== $check ) { + $enabled = hcaptcha()->settings()->is( $setting, 'login' ); + + if ( 'wp-login' === $form_id ) { + /** + * Filters whether the signed native login form is protected. + * + * Integrations such as Wordfence can deliberately disable native + * hCaptcha protection while leaving the WordPress setting enabled. + * This filter applies only after the native context is validated. + * + * @param bool $enabled Whether native login protection is enabled. + */ + $enabled = (bool) apply_filters( 'hcap_wp_login_protection_enabled', $enabled ); + } + + return [ $check, $enabled ]; + } + } + + return [ false, true ]; } /** @@ -209,11 +268,15 @@ private function is_login_verification_owner(): bool { * @noinspection PhpUnusedParameterInspection */ public function check_signature( $user, string $password ) { - if ( ! $this->is_wp_login_form() ) { + if ( ! $this->is_wp_login_form() || ! $this->is_login_enabled() ) { return $user; } - $check = HCaptcha::check_signature( static::class, 'login' ); + [ $check, $enabled ] = $this->get_login_signature(); + + if ( ! $enabled ) { + return $user; + } if ( $check && $this->can_skip_login_verification() ) { return $user; @@ -229,6 +292,18 @@ public function check_signature( $user, string $password ) { return $this->login_base_verify( $user, $password ); } + /** + * Whether this integration's login protection is enabled. + * + * Most integrations are loaded only when enabled. Always-loaded integrations + * override this method so they cannot verify or own a disabled login form. + * + * @return bool + */ + protected function is_login_enabled(): bool { + return true; + } + /** * Whether a valid signature can skip login verification. * diff --git a/src/php/AutoVerify/AutoVerify.php b/src/php/AutoVerify/AutoVerify.php index 61514fc4c..a350afe25 100644 --- a/src/php/AutoVerify/AutoVerify.php +++ b/src/php/AutoVerify/AutoVerify.php @@ -28,7 +28,10 @@ class AutoVerify { private const OPTION_PREFIX = 'hcaptcha_auto_verify_form_'; /** - * Maximum serialized transient size in bytes. + * Legacy transient size limit, retained for compatibility. + * + * @deprecated Registrations are stored in persistent options. + * @noinspection PhpUnused */ public const MAX_TRANSIENT_SIZE = 512 * 1024; @@ -266,7 +269,17 @@ private function get_registered_form_for_request(): ?array { return null; } - $path = $this->get_path( $request_uri ); + $path = $this->get_path( $request_uri ); + $target_key = $this->get_target_key( $request_uri ); + + if ( $target_key !== $path ) { + $registered_form = $this->get_registered_form( $target_key ); + + if ( null !== $registered_form ) { + return $registered_form; + } + } + $registered_form = $path ? $this->get_registered_form( $path ) : null; if ( null !== $registered_form ) { @@ -393,7 +406,7 @@ protected function register_forms( array $forms ): void { ]; } - $this->update_transient( $forms_data ); + $this->update_form_registrations( $forms_data ); } /** @@ -412,7 +425,65 @@ private function get_form_action( string $form ): string { $form_action = $form_action ?: $this->get_request_uri(); - return $this->get_path( $form_action ); + return $this->get_target_key( $form_action ); + } + + /** + * Distinguish WordPress query routes sharing the same URL path. + * + * @param string $url Form target or request URL. + * + * @return string + */ + private function get_target_key( string $url ): string { + $path = $this->get_path( $url ); + + if ( '' === $path && 0 === strpos( $url, '?' ) ) { + $path = $this->get_path( $this->get_request_uri() ); + } + + $query = wp_parse_url( $url, PHP_URL_QUERY ); + + if ( ! is_string( $query ) || '' === $query ) { + return $path; + } + + parse_str( $query, $query_vars ); + $post_id = $this->get_query_post_id( $query_vars ); + + if ( ! $post_id ) { + return $path; + } + + $home_path = $this->get_path( home_url( '/' ) ); + $index_path = ( '/' === $home_path ? '' : $home_path ) . '/index.php'; + + return $path === $home_path || $path === $index_path ? 'post:' . $post_id . ':' . $path : $path; + } + + /** + * Get the post selected by WordPress query variables. + * + * @param array $query_vars Parsed query variables. + * + * @return int + */ + private function get_query_post_id( array $query_vars ): int { + foreach ( [ 'page_id', 'p' ] as $key ) { + $value = $query_vars[ $key ] ?? null; + + if ( is_scalar( $value ) && ctype_digit( (string) $value ) ) { + return (int) $value; + } + } + + if ( ! empty( $query_vars['pagename'] ) && is_string( $query_vars['pagename'] ) ) { + $page = get_page_by_path( $query_vars['pagename'] ); + + return $page->ID ?? 0; + } + + return 0; } /** @@ -576,13 +647,14 @@ private function get_input_name( string $input ): ?string { } /** - * Update form data in transient. + * Persist form registrations and retire corresponding legacy transient entries. * - * @param array $forms_data Forms data to update in transient. + * @param array $forms_data Forms data to register. */ - protected function update_transient( array $forms_data ): void { + protected function update_form_registrations( array $forms_data ): void { $transient = get_transient( self::TRANSIENT ); - $registered_forms = $transient ?: []; + $legacy_forms = is_array( $transient ) ? $transient : []; + $registered_forms = $legacy_forms; foreach ( $forms_data as $form_data ) { $data = wp_parse_args( @@ -599,28 +671,36 @@ protected function update_transient( array $forms_data ): void { 'auto' => false, ] ); - $this->update_form_registration( $registered_forms, $data ); + if ( $this->update_form_registration( $registered_forms, $data ) ) { + unset( $legacy_forms[ $data['action'] ] ); + } } - $registered_forms = $this->limit_transient_size( $registered_forms ); + if ( ! is_array( $transient ) || $legacy_forms === $transient ) { + return; + } - set_transient( - self::TRANSIENT, - $registered_forms, - /** This filter is documented in wp-includes/pluggable.php. */ - apply_filters( 'nonce_life', constant( 'DAY_IN_SECONDS' ) ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound - ); + if ( $legacy_forms ) { + set_transient( + self::TRANSIENT, + $legacy_forms, + /** This filter is documented in wp-includes/pluggable.php. */ + apply_filters( 'nonce_life', constant( 'DAY_IN_SECONDS' ) ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + ); + } else { + delete_transient( self::TRANSIENT ); + } } /** - * Update one action in the transient and its persistent registration. + * Update one action in its persistent registration. * * @param array $registered_forms Registered forms. * @param array $data Form data. * - * @return void + * @return bool Whether a registration was persisted or removed. */ - private function update_form_registration( array &$registered_forms, array $data ): void { + private function update_form_registration( array &$registered_forms, array $data ): bool { $action = $data['action']; unset( $data['action'] ); @@ -651,16 +731,13 @@ private function update_form_registration( array &$registered_forms, array $data $action_forms[] = $data; } - // Move the action to the end of the array to mark it as recently used. - unset( $registered_forms[ $action ] ); - $registered_forms[ $action ] = array_values( $action_forms ); update_option( $option_name, $registered_forms[ $action ], false ); - return; + return true; } - $this->remove_form_registration( $registered_forms, $data, $action, $action_forms, $key ); + return $this->remove_form_registration( $registered_forms, $data, $action, $action_forms, $key ); } /** @@ -672,7 +749,7 @@ private function update_form_registration( array &$registered_forms, array $data * @param array $action_forms Forms registered for the action. * @param int|false $key Form key. * - * @return void + * @return bool Whether a registration was removed. */ private function remove_form_registration( array &$registered_forms, @@ -680,9 +757,9 @@ private function remove_form_registration( string $action, array $action_forms, $key - ): void { + ): bool { if ( false === $key || ( $action_forms[ $key ]['source'] ?? null ) !== ( $data['source'] ?? null ) ) { - return; + return false; } $this->remove_registered_form( $registered_forms, $action, $action_forms, $key ); @@ -694,6 +771,8 @@ private function remove_form_registration( } else { delete_option( $option_name ); } + + return true; } /** @@ -767,75 +846,6 @@ private function get_option_name( string $action ): string { return self::OPTION_PREFIX . hash( 'sha256', $action ); } - /** - * Limit the serialized transient size by removing the least recently used actions. - * - * @param array $registered_forms Registered forms. - * - * @return array - */ - private function limit_transient_size( array $registered_forms ): array { - $empty_array_size = $this->get_serialized_array_wrapper_size( 0 ); - - /** - * Filters the maximum serialized size of the auto-verify transient. - * - * @param int $max_size Maximum size in bytes. - */ - $max_size = (int) apply_filters( 'hcap_auto_verify_transient_max_size', self::MAX_TRANSIENT_SIZE ); - $max_size = max( $empty_array_size, $max_size ); - - $entry_sizes = []; - $payload_size = 0; - $registered_forms_count = count( $registered_forms ); - - foreach ( $registered_forms as $action => $action_forms ) { - $entry_size = $this->get_serialized_array_entry_size( $action, $action_forms ); - $entry_sizes[ $action ] = $entry_size; - $payload_size += $entry_size; - } - - while ( - $registered_forms && - $this->get_serialized_array_wrapper_size( $registered_forms_count ) + $payload_size > $max_size - ) { - $action = array_key_first( $registered_forms ); - $payload_size -= $entry_sizes[ $action ]; - - unset( $registered_forms[ $action ], $entry_sizes[ $action ] ); - - --$registered_forms_count; - } - - return $registered_forms; - } - - /** - * Get the serialized size of an array wrapper. - * - * @param int $count Number of array entries. - * - * @return int - */ - private function get_serialized_array_wrapper_size( int $count ): int { - return strlen( 'a:' . $count . ':{' ) + 1; - } - - /** - * Get the serialized size of an array entry. - * - * @param int|string $key Array key. - * @param mixed $value Array value. - * - * @return int - */ - private function get_serialized_array_entry_size( $key, $value ): int { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - $serialized_entry = serialize( [ $key => $value ] ); - - return strlen( $serialized_entry ) - $this->get_serialized_array_wrapper_size( 1 ); - } - /** * Get registered form. * @@ -856,6 +866,12 @@ protected function get_registered_form( string $path ): ?array { return null; } + $action_forms = $this->get_applicable_action_forms( $action_forms ); + + if ( ! $action_forms ) { + return null; + } + // Nonce is verified later, in \HCaptcha\Helpers\API::verify(). // phpcs:ignore WordPress.Security.NonceVerification.Missing $post_keys = array_keys( $_POST ); @@ -883,6 +899,30 @@ protected function get_registered_form( string $path ): ?array { return is_array( $registered_form ) ? $registered_form : null; } + /** + * Exclude Brevo forms when Brevo will not process the submission. + * + * @param array $action_forms Forms registered for the action. + * + * @return array + */ + private function get_applicable_action_forms( array $action_forms ): array { + if ( 'subscribe_form_submit' === Request::filter_input( INPUT_POST, 'sib_form_action' ) ) { + return $action_forms; + } + + return array_values( + array_filter( + $action_forms, + static function ( $form ): bool { + $source = (array) ( $form['args']['id']['source'] ?? [] ); + + return ! in_array( 'mailin/sendinblue.php', $source, true ); + } + ) + ); + } + /** * Process content and register the form for auto verification. * diff --git a/src/php/BBPress/Login.php b/src/php/BBPress/Login.php index de9fbed25..d9474430e 100644 --- a/src/php/BBPress/Login.php +++ b/src/php/BBPress/Login.php @@ -79,8 +79,17 @@ public function add_bbpress_captcha(): void { $this->bbpress_login_form = false; // Check the login status, because the class is always loading when bbPress is active. - if ( hcaptcha()->settings()->is( 'bbp_status', 'login' ) ) { + if ( $this->is_login_enabled() ) { $this->add_captcha(); } } + + /** + * Whether bbPress login protection is enabled. + * + * @return bool + */ + protected function is_login_enabled(): bool { + return hcaptcha()->settings()->is( 'bbp_status', 'login' ); + } } diff --git a/src/php/Divi/Login.php b/src/php/Divi/Login.php index 591e42a71..ee5ea6f60 100644 --- a/src/php/Divi/Login.php +++ b/src/php/Divi/Login.php @@ -82,15 +82,10 @@ public function add_hcaptcha_to_block( $block_content, array $block, WP_Block $i * @noinspection UnnecessaryCastingInspection */ private function add_divi_login_hcaptcha( string $output ): string { - if ( ! $this->is_login_limit_exceeded() ) { - return $output; - } - $hcaptcha = ''; - $theme = $this->get_active_divi_component(); // Check the login status, because the class is always loading when a Divi component is active. - if ( hcaptcha()->settings()->is( $theme . '_status', 'login' ) ) { + if ( $this->is_login_enabled() ) { ob_start(); $this->add_captcha(); @@ -102,7 +97,7 @@ private function add_divi_login_hcaptcha( string $output ): string { /** * Display hCaptcha signature. */ - do_action( 'hcap_signature' ); + do_action( 'hcap_signature', $this->get_active_divi_component() . '-login' ); $signatures = (string) ob_get_clean(); @@ -113,6 +108,15 @@ private function add_divi_login_hcaptcha( string $output ): string { return (string) preg_replace( $pattern, $replacement, $output ); } + /** + * Whether the active Divi component's login protection is enabled. + * + * @return bool + */ + protected function is_login_enabled(): bool { + return hcaptcha()->settings()->is( $this->get_active_divi_component() . '_status', 'login' ); + } + /** * Get expected hCaptcha widget id. * diff --git a/src/php/ElementorPro/Login.php b/src/php/ElementorPro/Login.php index ce2c87fff..2546af4c5 100644 --- a/src/php/ElementorPro/Login.php +++ b/src/php/ElementorPro/Login.php @@ -65,17 +65,10 @@ public function add_elementor_login_hcaptcha( Element_Base $element ): void { $form = (string) ob_get_clean(); - if ( ! $this->is_login_limit_exceeded() ) { - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $form; - - return; - } - $hcaptcha = ''; // Check the login status, because the class is always loading when Elementor Pro is active. - if ( hcaptcha()->settings()->is( 'elementor_pro_status', 'login' ) ) { + if ( $this->is_login_enabled() ) { ob_start(); $this->add_captcha(); @@ -88,7 +81,7 @@ public function add_elementor_login_hcaptcha( Element_Base $element ): void { /** * Display hCaptcha signature. */ - do_action( 'hcap_signature' ); + do_action( 'hcap_signature', 'elementor-login' ); $signatures = (string) ob_get_clean(); @@ -100,6 +93,15 @@ public function add_elementor_login_hcaptcha( Element_Base $element ): void { echo $form; } + /** + * Whether Elementor Pro login protection is enabled. + * + * @return bool + */ + protected function is_login_enabled(): bool { + return hcaptcha()->settings()->is( 'elementor_pro_status', 'login' ); + } + /** * Print inline styles. * diff --git a/src/php/Main.php b/src/php/Main.php index 480ae1c83..0eb1a05bd 100644 --- a/src/php/Main.php +++ b/src/php/Main.php @@ -1097,7 +1097,7 @@ public function register_recurring_actions(): void { $offset_hours = ( $gmt_offset > 0 ? '-' : '+' ) . absint( $gmt_offset ) . ' hours'; $tomorrow_6am = strtotime( 'tomorrow 06:00 am ' . $offset_hours ); - // Update Maxmind database every 15 days. + // Update MaxMind database every 15 days. as_schedule_recurring_action( $tomorrow_6am, 15 * DAY_IN_SECONDS, diff --git a/src/php/Wordfence/General.php b/src/php/Wordfence/General.php index 9a366763d..7e9f98ef9 100644 --- a/src/php/Wordfence/General.php +++ b/src/php/Wordfence/General.php @@ -38,6 +38,8 @@ public function __construct() { * @return void */ protected function init_hooks(): void { + add_filter( 'hcap_wp_login_protection_enabled', [ $this, 'filter_wp_login_protection_enabled' ] ); + if ( hcaptcha()->settings()->is( 'wordfence_status', 'login' ) ) { // Disable reCAPTCHA compatibility, otherwise a Wordfence login script fails and cannot show 2FA. hcaptcha()->settings()->set( 'recaptcha_compat_off', [ 'on' ] ); @@ -55,6 +57,17 @@ protected function init_hooks(): void { add_action( 'login_head', [ $this, 'print_inline_styles' ], 20 ); } + /** + * Apply the current Wordfence setting to signed native login protection. + * + * @param bool $enabled Whether native login protection is enabled. + * + * @return bool + */ + public function filter_wp_login_protection_enabled( bool $enabled ): bool { + return $enabled && hcaptcha()->settings()->is( 'wordfence_status', 'login' ); + } + /** * Remove Wordfence login CAPTCHA scripts. * diff --git a/tests/php/integration/Abstracts/LoginBaseTest.php b/tests/php/integration/Abstracts/LoginBaseTest.php index 54257b19b..2432ef4d4 100644 --- a/tests/php/integration/Abstracts/LoginBaseTest.php +++ b/tests/php/integration/Abstracts/LoginBaseTest.php @@ -12,6 +12,7 @@ use HCaptcha\Helpers\HCaptcha; use HCaptcha\Helpers\LoginAttempts; use HCaptcha\Tests\Integration\HCaptchaWPTestCase; +use HCaptcha\Wordfence\General as WordfenceGeneral; use HCaptcha\WP\Login as WPLogin; use ReflectionMethod; use tad\FunctionMocker\FunctionMocker; @@ -87,7 +88,9 @@ public function test_replayed_signature_uses_current_state( string $class_name ) $store->increment( $other_ip, $future - 16 * MINUTE_IN_SECONDS, 15 * MINUTE_IN_SECONDS ); self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); - update_option( 'hcaptcha_settings', [ 'login_limit' => 0 ] ); + $settings = get_option( 'hcaptcha_settings' ); + $settings['login_limit'] = 0; + update_option( 'hcaptcha_settings', $settings ); hcaptcha()->init_hooks(); self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); } @@ -316,6 +319,378 @@ public function dp_test_signature_verifier_skip_policy(): array { ]; } + /** + * Test the native form follows its own setting with other login integrations loaded. + * + * @param bool $wp_enabled Native login protection. + * @param bool $elementor_enabled Elementor login protection. + * @param bool $divi_enabled Divi login protection. + * @param bool $bbpress_enabled bbPress login protection. + * @param bool $wc_enabled WooCommerce login protection. + * @param bool|null $wordfence_enabled Wordfence login protection; null when Wordfence is absent. + * + * @dataProvider dp_test_native_login_settings + * + * @return void + */ + public function test_native_login_settings( + bool $wp_enabled, + bool $elementor_enabled, + bool $divi_enabled, + bool $bbpress_enabled, + bool $wc_enabled, + ?bool $wordfence_enabled = null + ): void { + $this->prepare_login_request( 0 ); + $settings = [ + 'login_limit' => 0, + 'wp_status' => $wp_enabled ? [ 'login' ] : [], + 'elementor_pro_status' => $elementor_enabled ? [ 'login' ] : [], + 'divi_status' => $divi_enabled ? [ 'login' ] : [], + 'bbp_status' => $bbpress_enabled ? [ 'login' ] : [], + 'woocommerce_status' => $wc_enabled ? [ 'login' ] : [], + 'wordfence_status' => $wordfence_enabled ? [ 'login' ] : [], + ]; + update_option( 'hcaptcha_settings', $settings ); + hcaptcha()->init_hooks(); + remove_all_filters( 'wp_authenticate_user' ); + remove_all_filters( 'hcap_wp_login_can_skip_verification' ); + remove_all_actions( 'login_form' ); + + new \HCaptcha\ElementorPro\Login(); + new \HCaptcha\Divi\Login(); + new \HCaptcha\BBPress\Login(); + + if ( $wc_enabled ) { + new \HCaptcha\WC\Login(); + } + + $wp_login = null; + + if ( $wp_enabled ) { + $wp_login = new WPLogin(); + new LoginOut(); + } + + $this->configure_wordfence_login_protection( $wp_login, $wordfence_enabled ); + + ob_start(); + do_action( 'login_form' ); + $form = (string) ob_get_clean(); + $this->submit_form_signatures( $form ); + $user = new WP_User( 1 ); + $result = apply_filters( 'wp_authenticate_user', $user, 'password' ); + + if ( ! $wp_enabled || false === $wordfence_enabled ) { + self::assertStringNotContainsString( 'prepare_verify_post( 'hcaptcha_login_nonce', 'hcaptcha_login' ); + $_POST[ HCaptcha::HCAPTCHA_WIDGET_ID ] = HCaptcha::widget_id_value( + [ + 'source' => [ 'WordPress' ], + 'form_id' => 'login', + ] + ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + + /** + * Data provider for test_native_login_settings(). + * + * @return array + */ + public function dp_test_native_login_settings(): array { + return [ + 'all-disabled' => [ false, false, false, false, false ], + 'woocommerce-only' => [ false, false, false, false, true ], + 'elementor-only' => [ false, true, false, false, false ], + 'divi-only' => [ false, false, true, false, false ], + 'bbpress-only' => [ false, false, false, true, false ], + 'all-third-party' => [ false, true, true, true, true ], + 'native-only' => [ true, false, false, false, false ], + 'all-enabled' => [ true, true, true, true, true ], + 'native-and-wc' => [ true, false, false, false, true ], + 'wordfence-on-native-and-wc' => [ true, false, false, false, true, true ], + 'wordfence-off-native-only' => [ true, false, false, false, false, false ], + 'wordfence-off-all-integrations' => [ true, true, true, true, true, false ], + 'wordfence-on-native-disabled' => [ false, true, true, true, true, true ], + ]; + } + + /** + * Test a native signature loses its exemption when Wordfence protection is enabled. + * + * @return void + */ + public function test_wordfence_native_signature_rechecks_protection(): void { + $this->prepare_login_request(); + remove_all_actions( 'login_form' ); + $wp_login = new WPLogin(); + new LoginOut(); + $sibling = new \HCaptcha\WC\Login(); + $this->configure_wordfence_login_protection( $wp_login, false ); + $user = new WP_User( 1 ); + + ob_start(); + do_action( 'login_form' ); + $form = (string) ob_get_clean(); + self::assertStringNotContainsString( 'submit_form_signatures( $form ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + $sibling->login_failed( 'test-user' ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + + $settings = get_option( 'hcaptcha_settings' ); + $settings['wordfence_status'] = [ 'login' ]; + update_option( 'hcaptcha_settings', $settings ); + hcaptcha()->settings()->init(); + + // Restore the native hooks as a new request with protection enabled would do. + add_action( 'login_form', [ $wp_login, 'add_captcha' ] ); + add_filter( 'wp_authenticate_user', [ $wp_login, 'check_signature' ], PHP_INT_MAX, 2 ); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + + ob_start(); + do_action( 'login_form' ); + $form = (string) ob_get_clean(); + self::assertStringContainsString( 'submit_form_signatures( $form ); + $this->prepare_verify_post( 'hcaptcha_login_nonce', 'hcaptcha_login' ); + $_POST[ HCaptcha::HCAPTCHA_WIDGET_ID ] = HCaptcha::widget_id_value( + [ + 'source' => [ 'WordPress' ], + 'form_id' => 'login', + ] + ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + + /** + * Test disabling native protection through Wordfence leaves frontend protection active. + * + * @return void + */ + public function test_wordfence_does_not_disable_frontend_protection(): void { + $this->prepare_login_request( 0 ); + remove_all_actions( 'hcap_signature' ); + $wp_login = new WPLogin(); + $owner = new \HCaptcha\ElementorPro\Login(); + $this->configure_wordfence_login_protection( $wp_login, false ); + $user = new WP_User( 1 ); + + ob_start(); + $owner->add_captcha(); + do_action( 'hcap_signature', 'elementor-login' ); + $this->submit_form_signatures( (string) ob_get_clean() ); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + + $this->prepare_verify_post( 'hcaptcha_login_nonce', 'hcaptcha_login' ); + $_POST[ HCaptcha::HCAPTCHA_WIDGET_ID ] = HCaptcha::widget_id_value( + [ + 'source' => HCaptcha::get_class_source( get_class( $owner ) ), + 'form_id' => 'login', + ] + ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + + /** + * Test Wordfence cannot exempt requests without a valid signed native context. + * + * @param string $variant Signature variant. + * + * @dataProvider dp_test_wordfence_invalid_native_signature + * + * @return void + */ + public function test_wordfence_invalid_native_signature( string $variant ): void { + $this->prepare_login_request( 0 ); + remove_all_actions( 'login_form' ); + $wp_login = new WPLogin(); + $sibling = new \HCaptcha\WC\Login(); + $this->configure_wordfence_login_protection( $wp_login, false ); + + ob_start(); + do_action( 'login_form' ); + $this->submit_form_signatures( (string) ob_get_clean() ); + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + $name = HCaptcha::HCAPTCHA_SIGNATURE . '-' . base64_encode( get_class( $sibling ) ); + + if ( 'missing' === $variant ) { + unset( $_POST[ $name ] ); + } elseif ( 'tampered' === $variant ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Mutate the signature generated by this test. + $_POST[ $name ] .= 'tampered'; + } else { + $this->submit_signature( $sibling ); + } + + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', new WP_User( 1 ), 'password' ) ); + } + + /** + * Data provider for test_wordfence_invalid_native_signature(). + * + * @return array + */ + public function dp_test_wordfence_invalid_native_signature(): array { + return [ + 'missing' => [ 'missing' ], + 'tampered' => [ 'tampered' ], + 'legacy' => [ 'legacy' ], + ]; + } + + /** + * Test a signed frontend context uses its current setting even with native protection enabled. + * + * @param string $context Signed rendering context. + * @param string $setting Login protection setting. + * + * @dataProvider dp_test_frontend_login_context + * + * @return void + */ + public function test_frontend_login_context_rechecks_setting( string $context, string $setting ): void { + $this->prepare_login_request( 0 ); + $settings = get_option( 'hcaptcha_settings' ); + $settings[ $setting ] = []; + update_option( 'hcaptcha_settings', $settings ); + hcaptcha()->settings()->init(); + remove_all_actions( 'hcap_signature' ); + new WPLogin(); + new \HCaptcha\WC\Login(); + new \HCaptcha\ElementorPro\Login(); + new \HCaptcha\Divi\Login(); + + ob_start(); + do_action( 'hcap_signature', $context ); + $this->submit_form_signatures( (string) ob_get_clean() ); + $user = new WP_User( 1 ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + + // A previously disabled form does not retain an exemption after protection is enabled. + $settings[ $setting ] = [ 'login' ]; + update_option( 'hcaptcha_settings', $settings ); + hcaptcha()->settings()->init(); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + + /** + * Data provider for test_frontend_login_context_rechecks_setting(). + * + * @return array + */ + public function dp_test_frontend_login_context(): array { + return [ + 'elementor' => [ 'elementor-login', 'elementor_pro_status' ], + 'divi' => [ 'divi-login', 'divi_status' ], + 'divi-builder' => [ 'divi_builder-login', 'divi_builder_status' ], + 'extra' => [ 'extra-login', 'extra_status' ], + ]; + } + + /** + * Test a disabled integration cannot authorize delegation with a harvested signature. + * + * @return void + */ + public function test_disabled_owner_cannot_authorize_skip(): void { + $this->prepare_login_request( 0 ); + $settings = get_option( 'hcaptcha_settings' ); + $settings['elementor_pro_status'] = []; + update_option( 'hcaptcha_settings', $settings ); + hcaptcha()->settings()->init(); + $owner = new \HCaptcha\ElementorPro\Login(); + $sibling = new WPLogin(); + $this->set_protected_property( $owner, 'hcaptcha_shown', true ); + $this->submit_signature( $owner ); + $this->submit_signature( $sibling ); + + self::assertFalse( $owner->allow_wp_login_skip_verification( false ) ); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', new WP_User( 1 ), 'password' ) ); + } + + /** + * Test a native signature cannot bypass an enabled native login after crossing the threshold. + * + * @return void + */ + public function test_native_signature_rechecks_current_settings_and_threshold(): void { + $this->prepare_login_request(); + remove_all_actions( 'login_form' ); + $subject = new WPLogin(); + $user = new WP_User( 1 ); + ob_start(); + do_action( 'login_form' ); + $this->submit_form_signatures( (string) ob_get_clean() ); + + self::assertTrue( HCaptcha::check_signature( WPLogin::class, 'wp-login' ) ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + $subject->login_failed( 'test-user' ); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + + /** + * Test a tampered native context cannot turn a protected frontend signature into an exemption. + * + * @return void + * @noinspection JsonEncodingApiUsageInspection + */ + public function test_tampered_native_context_is_rejected(): void { + $this->prepare_login_request( 0 ); + $settings = get_option( 'hcaptcha_settings' ); + $settings['wp_status'] = []; + update_option( 'hcaptcha_settings', $settings ); + hcaptcha()->settings()->init(); + $subject = new \HCaptcha\ElementorPro\Login(); + $name = $this->submit_signature( $subject ); + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- Mutate the signature generated by this test. + $value = explode( '-', $_POST[ $name ], 2 ); + // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + $id = json_decode( base64_decode( $value[0] ), true ); + $id['form_id'] = 'wp-login'; + $_POST[ $name ] = base64_encode( wp_json_encode( $id ) ) . '-' . $value[1]; + // phpcs:enable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', new WP_User( 1 ), 'password' ) ); + } + + /** + * Configure Wordfence and apply its real native hook removal when disabled. + * + * @param WPLogin|null $wp_login Native login integration. + * @param bool|null $wordfence_enabled Wordfence login protection; null when Wordfence is absent. + * + * @return void + */ + private function configure_wordfence_login_protection( ?WPLogin $wp_login, ?bool $wordfence_enabled ): void { + if ( null === $wordfence_enabled ) { + return; + } + + $wordfence = new WordfenceGeneral(); + + if ( $wordfence_enabled || ! $wp_login ) { + return; + } + + $main = hcaptcha(); + $loaded_classes = $this->get_protected_property( $main, 'loaded_classes' ); + $registered_classes = $loaded_classes; + $registered_classes[ WPLogin::class ] = $wp_login; + $this->set_protected_property( $main, 'loaded_classes', $registered_classes ); + $wordfence->remove_wp_login_hcaptcha_hooks(); + $this->set_protected_property( $main, 'loaded_classes', $loaded_classes ); + } + /** * Prepare the real shared authentication hook with a bounded adaptive state. * @@ -327,10 +702,20 @@ private function prepare_login_request( int $limit = 1 ): void { update_option( 'hcaptcha_settings', [ - 'login_limit' => $limit, - 'login_interval' => 15, + 'login_limit' => $limit, + 'login_interval' => 15, + 'wp_status' => [ 'login' ], + 'elementor_pro_status' => [ 'login' ], + 'divi_status' => [ 'login' ], + 'bbp_status' => [ 'login' ], ] ); + add_filter( + 'pre_option_template', + static function () { + return 'Divi'; + } + ); hcaptcha()->init_hooks(); remove_all_filters( 'wp_authenticate_user' ); remove_all_filters( 'hcap_wp_login_can_skip_verification' ); @@ -401,4 +786,20 @@ private function submit_signature( LoginBase $subject ): string { return $name; } + + /** + * Submit all signatures from a rendered login form. + * + * @param string $form Form HTML. + * + * @return void + */ + private function submit_form_signatures( string $form ): void { + preg_match_all( '/name="(hcaptcha-signature-[^"]+)"\s+value="([^"]+)"/', $form, $matches, PREG_SET_ORDER ); + self::assertNotEmpty( $matches ); + + foreach ( $matches as $match ) { + $_POST[ html_entity_decode( $match[1], ENT_QUOTES ) ] = html_entity_decode( $match[2], ENT_QUOTES ); + } + } } diff --git a/tests/php/integration/AutoVerify/AutoVerifyTest.php b/tests/php/integration/AutoVerify/AutoVerifyTest.php index 35e29d5ba..a461cff33 100644 --- a/tests/php/integration/AutoVerify/AutoVerifyTest.php +++ b/tests/php/integration/AutoVerify/AutoVerifyTest.php @@ -32,6 +32,7 @@ public function tearDown(): void { unset( $_SERVER['REQUEST_METHOD'], $_GET['pagename'], + $_GET['p'], $_GET['page_id'], $_GET['rest_route'], $GLOBALS['current_screen'] @@ -75,63 +76,159 @@ public function test_content_filter(): void { self::assertFalse( get_transient( $subject::TRANSIENT ) ); apply_filters( 'the_content', $content ); - self::assertSame( $expected, get_transient( $subject::TRANSIENT ) ); + $path = array_key_first( $expected ); + self::assertSame( $expected[ $path ], $this->get_registered_action_forms( $path ) ); + self::assertFalse( get_transient( $subject::TRANSIENT ) ); } /** - * Test content_filter() limits the transient size using LRU eviction. + * An actionless form on a query route must not claim unrelated home requests. * - * @return void + * @noinspection PhpArrayWriteIsNotUsedInspection */ - public function test_content_filter_limits_transient_size_with_lru_eviction(): void { - $content = $this->get_test_content(); + public function test_query_route_form_does_not_claim_home(): void { + $page_id = $this->factory()->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + ] + ); $subject = new AutoVerify(); - $subject->init(); - $_SERVER['REQUEST_URI'] = '/path-one'; + $_SERVER['REQUEST_URI'] = '/?page_id=' . $page_id . '&preview=true'; + $_GET['page_id'] = (string) $page_id; + apply_filters( 'the_content', $this->get_test_content() ); + + $target_key = 'post:' . $page_id . ':/'; + $option_name = 'hcaptcha_auto_verify_form_' . hash( 'sha256', $target_key ); + self::assertCount( 1, get_option( $option_name ) ); + self::assertFalse( get_option( 'hcaptcha_auto_verify_form_' . hash( 'sha256', '/' ), false ) ); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REQUEST_URI'] = '/'; + $_POST['foo'] = 'bar'; + unset( $_GET['page_id'] ); + $subject->verify(); + + self::assertSame( [ 'foo' => 'bar' ], $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + $_SERVER['REQUEST_URI'] = '/?page_id=' . $page_id . '&utm_source=test'; + $_GET['page_id'] = (string) $page_id; + $_POST = [ 'test_input' => 'value' ]; + $die_arr = []; + + add_filter( + 'wp_die_handler', + static function () use ( &$die_arr ) { + return static function ( $message, $title, $args ) use ( &$die_arr ) { + $die_arr = [ $message, $title, $args ]; + }; + } + ); + + $subject->verify(); + + self::assertSame( 'Bad hCaptcha signature!', $die_arr[0] ?? null ); + self::assertSame( 403, $die_arr[2]['response'] ?? null ); + } + + /** + * An explicit root action on a query page still targets the home route. + */ + public function test_explicit_home_action_on_query_page(): void { + $page_id = $this->factory()->post->create( + [ + 'post_type' => 'page', + 'post_status' => 'publish', + ] + ); + $subject = new AutoVerify(); + $subject->init(); + + $_SERVER['REQUEST_URI'] = '/?page_id=' . $page_id; + $_GET['page_id'] = (string) $page_id; + $content = str_replace( '
', '', $this->get_test_content() ); apply_filters( 'the_content', $content ); - $registered_forms = get_transient( AutoVerify::TRANSIENT ); - $action_forms = $registered_forms['/path-one']; - $two_paths = [ - '/path-one' => $action_forms, - '/path-two' => $action_forms, - ]; - $max_size = strlen( maybe_serialize( $two_paths ) ); - $size_filter = static function () use ( $max_size ): int { - return $max_size; - }; + self::assertCount( 1, $this->get_registered_action_forms( '/' ) ); + self::assertSame( [], $this->get_registered_action_forms( 'post:' . $page_id . ':/' ) ); + } - add_filter( 'hcap_auto_verify_transient_max_size', $size_filter ); + /** + * A real home form still protects posts to its path with query parameters. + * + * @noinspection PhpArrayWriteIsNotUsedInspection + */ + public function test_home_registration_protects_query_route(): void { + $page_id = $this->factory()->post->create(); + $forms = $this->get_test_registered_forms(); + $form = $forms[ array_key_first( $forms ) ][0]; - try { - $_SERVER['REQUEST_URI'] = '/path-two'; + update_option( 'hcaptcha_auto_verify_form_' . hash( 'sha256', '/' ), [ $form ], false ); - apply_filters( 'the_content', $content ); + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_SERVER['REQUEST_URI'] = '/?p=' . $page_id; + $_GET['p'] = (string) $page_id; + $_POST = [ 'foo' => 'bar' ]; - $_SERVER['REQUEST_URI'] = '/path-one'; + $subject = new AutoVerify(); - apply_filters( 'the_content', $content ); + self::assertSame( [], $this->set_method_accessibility( $subject, 'get_registered_form_for_request' )->invoke( $subject ) ); + } - $_SERVER['REQUEST_URI'] = '/path-new'; + /** + * Adding a page selector to another endpoint cannot bypass its protection. + */ + public function test_page_selector_does_not_change_other_action(): void { + $subject = new AutoVerify(); + $method = $this->set_method_accessibility( $subject, 'get_target_key' ); + + self::assertSame( '/form', $method->invoke( $subject, '/form/?p=123' ) ); + self::assertSame( '/form', $method->invoke( $subject, '/form/?page_id=123' ) ); + self::assertSame( 'post:123:/', $method->invoke( $subject, '/?p=123' ) ); + } + + /** + * A legacy Brevo registration must only apply to Brevo submissions. + * + * @noinspection PhpArrayWriteIsNotUsedInspection + */ + public function test_legacy_brevo_home_registration_ignores_other_posts(): void { + $registered_form = $this->get_test_registered_forms(); + $form = $registered_form[ array_key_first( $registered_form ) ][0]; + $form['args']['id']['source'] = [ 'mailin/sendinblue.php' ]; + $option_name = 'hcaptcha_auto_verify_form_' . hash( 'sha256', '/' ); + $subject = new AutoVerify(); - apply_filters( 'the_content', $content ); + update_option( $option_name, [ $form ], false ); + $_POST = [ 'edd_action' => 'activate_license' ]; - $new_action_forms = $action_forms; - $new_action_forms[0]['source'] = '/path-new'; - $expected = [ - '/path-one' => $action_forms, - '/path-new' => $new_action_forms, - ]; - $actual = get_transient( AutoVerify::TRANSIENT ); + self::assertNull( $this->set_method_accessibility( $subject, 'get_registered_form' )->invoke( $subject, '/' ) ); - self::assertSame( $expected, $actual ); - self::assertLessThanOrEqual( $max_size, strlen( maybe_serialize( $actual ) ) ); - } finally { - remove_filter( 'hcap_auto_verify_transient_max_size', $size_filter ); - } + $_POST['sib_form_action'] = 'subscribe_form_submit'; + + self::assertSame( [], $this->set_method_accessibility( $subject, 'get_registered_form' )->invoke( $subject, '/' ) ); + } + + /** + * Rendering several forms persists them without creating a transient. + */ + public function test_content_filter_does_not_write_transient(): void { + $content = $this->get_test_content(); + $subject = new AutoVerify(); + + $subject->init(); + + $_SERVER['REQUEST_URI'] = '/path-one'; + apply_filters( 'the_content', $content ); + $_SERVER['REQUEST_URI'] = '/path-two'; + apply_filters( 'the_content', $content ); + + self::assertCount( 1, $this->get_registered_action_forms( '/path-one' ) ); + self::assertCount( 1, $this->get_registered_action_forms( '/path-two' ) ); + self::assertFalse( get_transient( AutoVerify::TRANSIENT ) ); } /** @@ -153,7 +250,9 @@ public function test_widget_block_content_filter(): void { self::assertFalse( get_transient( $subject::TRANSIENT ) ); apply_filters( 'widget_block_content', $content, [], $wp_widget_block ); - self::assertSame( $expected, get_transient( $subject::TRANSIENT ) ); + $path = array_key_first( $expected ); + self::assertSame( $expected[ $path ], $this->get_registered_action_forms( $path ) ); + self::assertFalse( get_transient( $subject::TRANSIENT ) ); } /** @@ -179,7 +278,9 @@ public function test_content_filter_with_action(): void { self::assertFalse( get_transient( $subject::TRANSIENT ) ); apply_filters( 'the_content', $content ); - self::assertSame( $expected, get_transient( $subject::TRANSIENT ) ); + $path = array_key_first( $expected ); + self::assertSame( $expected[ $path ], $this->get_registered_action_forms( $path ) ); + self::assertFalse( get_transient( $subject::TRANSIENT ) ); } /** @@ -196,7 +297,7 @@ public function test_content_filter_without_form_action(): void { self::assertFalse( get_transient( $subject::TRANSIENT ) ); apply_filters( 'the_content', $content ); - self::assertSame( [], get_transient( $subject::TRANSIENT ) ); + self::assertFalse( get_transient( $subject::TRANSIENT ) ); } /** @@ -290,34 +391,17 @@ public function test_verify_form_when_forms_on_another_uri_are_registered(): voi } /** - * A rendered form remains protected when its transient expires or is evicted. - * - * @param bool $evict Whether to evict the entry rather than delete the transient. - * - * @dataProvider dp_test_verify_form_without_transient_entry + * A rendered form remains protected without a transient entry. */ - public function test_verify_form_without_transient_entry( bool $evict ): void { + public function test_verify_form_without_transient_entry(): void { $subject = new AutoVerify(); $subject->init(); $_SERVER['REQUEST_URI'] = '/path-one'; apply_filters( 'the_content', $this->get_test_content() ); - if ( $evict ) { - $max_size = strlen( maybe_serialize( get_transient( AutoVerify::TRANSIENT ) ) ); - $filter = static function () use ( $max_size ): int { - return $max_size; - }; - - add_filter( 'hcap_auto_verify_transient_max_size', $filter ); - $_SERVER['REQUEST_URI'] = '/path-two'; - apply_filters( 'the_content', $this->get_test_content() ); - remove_filter( 'hcap_auto_verify_transient_max_size', $filter ); - } else { - delete_transient( AutoVerify::TRANSIENT ); - } - - self::assertEmpty( get_transient( AutoVerify::TRANSIENT )['/path-one'] ?? [] ); + self::assertFalse( get_transient( AutoVerify::TRANSIENT ) ); + self::assertCount( 1, $this->get_registered_action_forms( '/path-one' ) ); $_SERVER['REQUEST_METHOD'] = 'POST'; $_SERVER['REQUEST_URI'] = '/path-one'; @@ -339,18 +423,6 @@ static function () use ( &$die_arr ) { self::assertSame( [], $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } - /** - * Data provider for test_verify_form_without_transient_entry(). - * - * @return array - */ - public function dp_test_verify_form_without_transient_entry(): array { - return [ - 'expired' => [ false ], - 'evicted' => [ true ], - ]; - } - /** * Rendering a matching form on another page cannot remove its registration. */ @@ -361,8 +433,8 @@ public function test_non_owner_render_cannot_remove_registration(): void { $_SERVER['REQUEST_URI'] = '/victim'; apply_filters( 'the_content', $this->get_test_content() ); - $registered_forms = get_transient( AutoVerify::TRANSIENT ); - $widget_id = $registered_forms['/victim'][0]['widget_id']; + $registered_forms = $this->get_registered_action_forms( '/victim' ); + $widget_id = $registered_forms[0]['widget_id']; $attacker_form = '' . '' . '
'; @@ -371,10 +443,10 @@ public function test_non_owner_render_cannot_remove_registration(): void { $attacker = new AutoVerify(); $attacker->content_filter( $attacker_form ); - self::assertArrayHasKey( '/victim', get_transient( AutoVerify::TRANSIENT ) ); + self::assertCount( 1, $this->get_registered_action_forms( '/victim' ) ); $attacker->content_filter( str_replace( '', '', $attacker_form ) ); - self::assertSame( $registered_forms['/victim'], get_transient( AutoVerify::TRANSIENT )['/victim'] ); + self::assertSame( $registered_forms, $this->get_registered_action_forms( '/victim' ) ); } /** @@ -389,14 +461,14 @@ public function test_non_owner_plain_permalink_cannot_remove_registration(): voi $_SERVER['REQUEST_URI'] = '/?page_id=' . $victim_id; apply_filters( 'the_content', $this->get_test_content() ); - $registered_forms = get_transient( AutoVerify::TRANSIENT ); + $registered_forms = $this->get_registered_action_forms( 'post:' . $victim_id . ':/' ); $attacker_form = '
' . '
'; $_SERVER['REQUEST_URI'] = '/?page_id=' . $attacker_id; ( new AutoVerify() )->content_filter( $attacker_form ); - self::assertSame( $registered_forms['/'], get_transient( AutoVerify::TRANSIENT )['/'] ); + self::assertSame( $registered_forms, $this->get_registered_action_forms( 'post:' . $victim_id . ':/' ) ); } /** @@ -409,8 +481,8 @@ public function test_non_owner_auto_form_cannot_replace_registration(): void { $_SERVER['REQUEST_URI'] = '/victim'; apply_filters( 'the_content', $this->get_test_content() ); - $registered_forms = get_transient( AutoVerify::TRANSIENT ); - $victim_form = $registered_forms['/victim'][0]; + $registered_forms = $this->get_registered_action_forms( '/victim' ); + $victim_form = $registered_forms[0]; $attacker_form = '
' . '' . '
'; @@ -439,15 +511,15 @@ public function test_owner_render_can_remove_registration(): void { $_SERVER['REQUEST_URI'] = '/victim'; apply_filters( 'the_content', $this->get_test_content() ); - $registered_forms = get_transient( AutoVerify::TRANSIENT ); - $widget_id = $registered_forms['/victim'][0]['widget_id']; + $registered_forms = $this->get_registered_action_forms( '/victim' ); + $widget_id = $registered_forms[0]['widget_id']; $regular_form = '
' . '' . '
'; ( new AutoVerify() )->content_filter( $regular_form ); - self::assertSame( [], get_transient( AutoVerify::TRANSIENT ) ); + self::assertSame( [], $this->get_registered_action_forms( '/victim' ) ); self::assertNull( $this->set_method_accessibility( $subject, 'get_registered_form' )->invoke( $subject, '/victim' ) ); } @@ -469,9 +541,10 @@ public function test_legacy_registration_is_replaced_on_render(): void { $subject->init(); apply_filters( 'the_content', $this->get_test_content() ); - $updated = get_transient( AutoVerify::TRANSIENT ); - self::assertCount( 1, $updated[ $path ] ); - self::assertSame( $request_uri, $updated[ $path ][0]['source'] ); + $updated = $this->get_registered_action_forms( $path ); + self::assertCount( 1, $updated ); + self::assertSame( $request_uri, $updated[0]['source'] ); + self::assertFalse( get_transient( AutoVerify::TRANSIENT ) ); $regular_form = '
' . '' . @@ -482,6 +555,24 @@ public function test_legacy_registration_is_replaced_on_render(): void { self::assertNull( $this->set_method_accessibility( $subject, 'get_registered_form' )->invoke( $subject, $path ) ); } + /** + * Migrating one legacy action preserves other transient registrations. + */ + public function test_legacy_registration_preserves_other_actions(): void { + $registered_forms = $this->get_test_registered_forms(); + $path = array_key_first( $registered_forms ); + $registered_forms['/other'] = $registered_forms[ $path ]; + set_transient( AutoVerify::TRANSIENT, $registered_forms ); + + $_SERVER['REQUEST_URI'] = $this->get_test_request_uri(); + $subject = new AutoVerify(); + $subject->init(); + apply_filters( 'the_content', $this->get_test_content() ); + + self::assertCount( 1, $this->get_registered_action_forms( $path ) ); + self::assertSame( [ '/other' => $registered_forms['/other'] ], get_transient( AutoVerify::TRANSIENT ) ); + } + /** * A valid submission still succeeds after the transient entry disappears. */ @@ -824,6 +915,17 @@ static function () use ( &$die_arr ) { self::assertSame( $expected, $die_arr ); } + /** + * Get persisted registrations for one action. + * + * @param string $action Form action. + * + * @return array + */ + private function get_registered_action_forms( string $action ): array { + return (array) get_option( 'hcaptcha_auto_verify_form_' . hash( 'sha256', $action ), [] ); + } + /** * Get test request URI. * diff --git a/tests/php/integration/BBPress/LoginTest.php b/tests/php/integration/BBPress/LoginTest.php index 9f786176f..035f143c0 100644 --- a/tests/php/integration/BBPress/LoginTest.php +++ b/tests/php/integration/BBPress/LoginTest.php @@ -5,8 +5,12 @@ * @package HCaptcha\Tests */ +// phpcs:ignore Generic.Commenting.DocComment.MissingShort +/** @noinspection PhpUndefinedClassInspection */ + namespace HCaptcha\Tests\Integration\BBPress; +use BBP_Shortcodes; use HCaptcha\BBPress\Login; use HCaptcha\Helpers\HCaptcha; use HCaptcha\Tests\Integration\HCaptchaPluginWPTestCase; @@ -48,13 +52,15 @@ class LoginTest extends HCaptchaPluginWPTestCase { /** * Test the login form rendered by the live bbPress shortcode. + * + * @noinspection PhpUndefinedFunctionInspection */ public function test_live_login_form(): void { hcaptcha()->settings()->set( 'bbp_status', 'login' ); new Login(); - $shortcodes_file = wp_normalize_path( ( new ReflectionClass( \BBP_Shortcodes::class ) )->getFileName() ); + $shortcodes_file = wp_normalize_path( ( new ReflectionClass( BBP_Shortcodes::class ) )->getFileName() ); $template = bbp_get_template_part( 'form', 'user-login' ); ob_start(); @@ -143,6 +149,8 @@ public function test_widget_login_form(): void { * @return void */ public function test_allow_wp_login_skip_verification(): void { + update_option( 'hcaptcha_settings', [ 'bbp_status' => [ 'login' ] ] ); + hcaptcha()->init_hooks(); $subject = new Login(); self::assertTrue( $subject->allow_wp_login_skip_verification( true ) ); @@ -178,6 +186,8 @@ public function test_allow_wp_login_skip_verification(): void { * @return void */ public function test_defer_auto_verification(): void { + update_option( 'hcaptcha_settings', [ 'bbp_status' => [ 'login' ] ] ); + hcaptcha()->init_hooks(); $registered_form = []; $subject = new Login(); $login_path = (string) wp_parse_url( wp_login_url(), PHP_URL_PATH ); diff --git a/tests/php/integration/Divi/LoginTest.php b/tests/php/integration/Divi/LoginTest.php index a919ce3ba..c466fa5ba 100644 --- a/tests/php/integration/Divi/LoginTest.php +++ b/tests/php/integration/Divi/LoginTest.php @@ -9,6 +9,7 @@ use HCaptcha\Divi\Login; use HCaptcha\Helpers\HCaptcha; +use HCaptcha\Helpers\LoginAttempts; use HCaptcha\Tests\Integration\HCaptchaPluginWPTestCase; use Mockery; use ReflectionException; @@ -59,6 +60,17 @@ public function setUp(): void { $this->set_protected_property( hcaptcha(), 'supported_forms', null ); } + /** + * Clear login-attempt state after each test. + * + * @return void + */ + public function tearDown(): void { + LoginAttempts::delete_all(); + + parent::tearDown(); + } + /** * Test constructor and init_hooks(). * @@ -198,6 +210,46 @@ public function test_add_hcaptcha_when_login_limit_is_not_exceeded(): void { self::assertSame( $output, $subject->add_hcaptcha_to_shortcode( $output, Login::TAG ) ); } + /** + * Test a real below-threshold Divi form cannot preserve its exemption after a failed login. + * + * @return void + */ + public function test_rendered_below_threshold_form_requires_captcha_after_failure(): void { + update_option( + 'hcaptcha_settings', + [ + 'divi_status' => [ 'login' ], + 'wp_status' => [], + 'login_limit' => 1, + 'login_interval' => 15, + ] + ); + hcaptcha()->init_hooks(); + remove_all_actions( 'hcap_signature' ); + remove_all_filters( 'wp_authenticate_user' ); + remove_all_filters( 'hcap_wp_login_can_skip_verification' ); + $subject = new Login(); + $user = new WP_User( 1 ); + $this->set_protected_property( $subject, 'ip', '203.0.113.21' ); + $form = $subject->add_hcaptcha_to_shortcode( '

', Login::TAG ); + + self::assertStringNotContainsString( 'login_failed( 'test-user' ); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + /** * Test add_hcaptcha_to_block() with output from the live Login module. * diff --git a/tests/php/integration/ElementorPro/LoginTest.php b/tests/php/integration/ElementorPro/LoginTest.php index a6f20a842..b7553f674 100644 --- a/tests/php/integration/ElementorPro/LoginTest.php +++ b/tests/php/integration/ElementorPro/LoginTest.php @@ -152,7 +152,7 @@ public function test_render(): void { $hcaptcha = $this->get_hcap_form( $args ); $hcaptcha = '
' . $hcaptcha . '
'; - $signatures = HCaptcha::get_signature( Login::class, 'login', true ); + $signatures = HCaptcha::get_signature( Login::class, 'elementor-login', true ); $submit_div = '
'; $expected = str_replace( $submit_div, $hcaptcha . $signatures . "\n" . $submit_div, $form ); @@ -219,6 +219,46 @@ public function test_render_when_login_limit_is_not_exceeded(): void { self::assertSame( $form, ob_get_clean() ); } + /** + * Test a real below-threshold form carries a signature that cannot preserve its exemption. + * + * @return void + * @noinspection PhpParamsInspection + */ + public function test_rendered_below_threshold_form_requires_captcha_after_failure(): void { + update_option( + 'hcaptcha_settings', + [ + 'elementor_pro_status' => [ 'login' ], + 'wp_status' => [], + 'login_limit' => 1, + 'login_interval' => 15, + ] + ); + hcaptcha()->init_hooks(); + remove_all_actions( 'hcap_signature' ); + remove_all_filters( 'wp_authenticate_user' ); + remove_all_filters( 'hcap_wp_login_can_skip_verification' ); + $subject = new Login(); + $element = new ElementorLogin(); + $user = new WP_User( 1 ); + $this->set_protected_property( $subject, 'ip', '203.0.113.20' ); + + ob_start(); + $subject->before_render( $element ); + echo '
'; + $subject->add_elementor_login_hcaptcha( $element ); + $form = (string) ob_get_clean(); + + self::assertStringNotContainsString( 'submit_signature( $form ); + $this->mark_native_login_request(); + self::assertTrue( HCaptcha::check_signature( Login::class, 'elementor-login' ) ); + self::assertSame( $user, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + $subject->login_failed( 'test-user' ); + self::assertInstanceOf( WP_Error::class, apply_filters( 'wp_authenticate_user', $user, 'password' ) ); + } + /** * Test a valid below-threshold signature cannot be replayed after the threshold is crossed. * @@ -231,8 +271,9 @@ public function test_replayed_below_threshold_signature_requires_captcha(): void update_option( 'hcaptcha_settings', [ - 'login_limit' => 1, - 'login_interval' => 15, + 'login_limit' => 1, + 'login_interval' => 15, + 'elementor_pro_status' => [ 'login' ], ] ); hcaptcha()->init_hooks(); @@ -267,8 +308,9 @@ public function test_false_signature_cannot_skip_when_login_limit_is_zero(): voi update_option( 'hcaptcha_settings', [ - 'login_limit' => 0, - 'login_interval' => 15, + 'login_limit' => 0, + 'login_interval' => 15, + 'elementor_pro_status' => [ 'login' ], ] ); hcaptcha()->init_hooks(); @@ -300,8 +342,9 @@ public function test_solved_challenge_above_threshold(): void { update_option( 'hcaptcha_settings', [ - 'login_limit' => 1, - 'login_interval' => 15, + 'login_limit' => 1, + 'login_interval' => 15, + 'elementor_pro_status' => [ 'login' ], ] ); hcaptcha()->init_hooks(); @@ -315,7 +358,7 @@ public function test_solved_challenge_above_threshold(): void { $captcha = (string) ob_get_clean(); ob_start(); - $subject->display_signature(); + $subject->display_signature( 'elementor-login' ); $this->submit_signature( (string) ob_get_clean() ); $this->prepare_verify_post( 'hcaptcha_login_nonce', 'hcaptcha_login' ); @@ -328,7 +371,7 @@ public function test_solved_challenge_above_threshold(): void { $this->mark_native_login_request(); self::assertStringContainsString( 'check_signature( $user, 'password' ) ); } diff --git a/tests/php/integration/WP/LoginAttemptsConcurrencyTest.php b/tests/php/integration/WP/LoginAttemptsConcurrencyTest.php index 7fac790ae..5dbd5a7e3 100644 --- a/tests/php/integration/WP/LoginAttemptsConcurrencyTest.php +++ b/tests/php/integration/WP/LoginAttemptsConcurrencyTest.php @@ -52,7 +52,7 @@ public function tearDown(): void { */ public function test_concurrent_failures_retain_every_increment_and_enforce_threshold( bool $persistent_cache ): void { $address = '198.51.100.90'; - $other_address = '203.0.113.90'; + $other_address = $this->address_in_distinct_slot( $address ); $now = time(); $jobs = []; @@ -121,7 +121,7 @@ public function test_concurrent_failures_retain_every_increment_and_enforce_thre */ public function test_concurrent_expiry_and_saturation_are_bounded( bool $persistent_cache ): void { $expired_address = '198.51.100.91'; - $saturated_address = '203.0.113.91'; + $saturated_address = $this->address_in_distinct_slot( $expired_address ); $now = time(); $jobs = []; @@ -159,7 +159,7 @@ public function test_concurrent_expiry_and_saturation_are_bounded( bool $persist */ public function test_concurrent_success_reset_uses_compare_and_swap( bool $persistent_cache ): void { $address = '198.51.100.92'; - $other_address = '203.0.113.92'; + $other_address = $this->address_in_distinct_slot( $address ); $now = time(); self::assertNotSame( $this->option_name( $address ), $this->option_name( $other_address ) ); @@ -634,6 +634,28 @@ private function option_name( string $address ): string { return LoginAttempts::OPTION_PREFIX . sprintf( '%04d', $slot ); } + /** + * Find a second test address whose storage slot differs under the current salt. + * + * @param string $address First address. + * + * @return string + * @throws RuntimeException When no distinct slot can be found. + */ + private function address_in_distinct_slot( string $address ): string { + $option_name = $this->option_name( $address ); + + for ( $suffix = 1; $suffix <= 254; ++$suffix ) { + $candidate = '203.0.113.' . $suffix; + + if ( $option_name !== $this->option_name( $candidate ) ) { + return $candidate; + } + } + + throw new RuntimeException( 'Cannot find a test address in a distinct login-attempt slot.' ); + } + /** * Assert the shared cache path was invalidated for every address. * diff --git a/tests/php/integration/Wordfence/GeneralTest.php b/tests/php/integration/Wordfence/GeneralTest.php index 7e8e4359e..a80e6ac4d 100644 --- a/tests/php/integration/Wordfence/GeneralTest.php +++ b/tests/php/integration/Wordfence/GeneralTest.php @@ -7,6 +7,7 @@ // phpcs:disable Generic.Commenting.DocComment.MissingShort /** @noinspection PhpLanguageLevelInspection */ +/** @noinspection PhpUndefinedNamespaceInspection */ /** @noinspection PhpUndefinedClassInspection */ // phpcs:enable Generic.Commenting.DocComment.MissingShort @@ -119,6 +120,11 @@ public function test_init_hooks( string $wordfence_status ): void { $subject = new General(); + self::assertSame( + 10, + has_filter( 'hcap_wp_login_protection_enabled', [ $subject, 'filter_wp_login_protection_enabled' ] ) + ); + if ( 'login' === $wordfence_status ) { self::assertSame( [ 'on' ], hcaptcha()->settings()->get( 'recaptcha_compat_off' ) ); self::assertSame( 20, has_action( 'login_enqueue_scripts', [ $subject, 'remove_wordfence_recaptcha_script' ] ) ); diff --git a/tests/php/unit/AutoVerify/AutoVerifyTest.php b/tests/php/unit/AutoVerify/AutoVerifyTest.php index 94415700f..cdbe52488 100644 --- a/tests/php/unit/AutoVerify/AutoVerifyTest.php +++ b/tests/php/unit/AutoVerify/AutoVerifyTest.php @@ -761,7 +761,7 @@ public function test_register_forms_with_empty_forms(): void { $subject = Mockery::mock( AutoVerify::class )->makePartial(); $subject->shouldAllowMockingProtectedMethods(); - $subject->shouldReceive( 'update_transient' )->with( [] )->once(); + $subject->shouldReceive( 'update_form_registrations' )->with( [] )->once(); $subject->register_forms( [] ); } @@ -815,10 +815,10 @@ static function ( $url, $component ) { $this->set_protected_property( $subject, 'registry', $registry ); $subject->shouldAllowMockingProtectedMethods(); - $subject->shouldReceive( 'update_transient' )->with( [] )->once(); // Case 1. - $subject->shouldReceive( 'update_transient' )->with( $expected )->once(); // Case 2. - $subject->shouldReceive( 'update_transient' )->with( $expected_without_inputs )->once(); // Case 3. - $subject->shouldReceive( 'update_transient' )->with( $expected_without_auto )->once(); // Case 4. + $subject->shouldReceive( 'update_form_registrations' )->with( [] )->once(); // Case 1. + $subject->shouldReceive( 'update_form_registrations' )->with( $expected )->once(); // Case 2. + $subject->shouldReceive( 'update_form_registrations' )->with( $expected_without_inputs )->once(); // Case 3. + $subject->shouldReceive( 'update_form_registrations' )->with( $expected_without_auto )->once(); // Case 4. // Case 1. Update transient to be called with []. $subject->register_forms( $forms ); @@ -851,25 +851,15 @@ static function ( $url, $component ) { } /** - * Test update_transient(). - * - * @param mixed $transient Transient. - * @param array $forms_data Forms data. - * @param array $expected Expected. - * - * @return void - * @dataProvider dp_test_update_transient - * @throws ReflectionException ReflectionException. + * New registrations are persisted without writing a duplicate transient. */ - public function test_update_transient( $transient, array $forms_data, array $expected ): void { - $day_in_seconds = 24 * 60 * 60; + public function test_update_form_registrations_does_not_write_transient(): void { + $data = [ + 'action' => '/autoverify', + 'inputs' => [ 'test_input' ], + 'args' => [ 'auto' => true ], + ]; - FunctionMocker::replace( - 'constant', - static function ( $name ) use ( $day_in_seconds ) { - return 'DAY_IN_SECONDS' === $name ? $day_in_seconds : 0; - } - ); WP_Mock::userFunction( 'wp_parse_args' )->andReturnUsing( static function ( $args, $defaults ) { return array_merge( $defaults, $args ); @@ -878,275 +868,26 @@ static function ( $args, $defaults ) { WP_Mock::userFunction( 'get_transient' ) ->with( AutoVerify::TRANSIENT ) ->once() - ->andReturn( $transient ); - WP_Mock::userFunction( 'get_option' )->andReturn( false ); - WP_Mock::userFunction( 'update_option' )->andReturn( true ); - WP_Mock::userFunction( 'delete_option' )->andReturn( true ); - WP_Mock::userFunction( 'set_transient' ) - ->with( AutoVerify::TRANSIENT, $expected, $day_in_seconds ) - ->once(); - - $subject = Mockery::mock( AutoVerify::class )->makePartial(); - $method = 'update_transient'; - - $subject->$method( $forms_data ); - } - - /** - * Test update_transient() limits the size and preserves recently used actions. - * - * @return void - */ - public function test_update_transient_limits_size_with_lru_eviction(): void { - $day_in_seconds = 24 * 60 * 60; - $args = [ - 'action' => 'hcaptcha_action', - 'name' => 'hcaptcha_nonce', - 'auto' => true, - ]; - $action_forms = [ - [ - 'inputs' => [ 'test_input' ], - 'args' => $args, - ], - ]; - $transient = [ - '/oldest' => $action_forms, - '/middle' => $action_forms, - '/newest' => $action_forms, - ]; - $forms_data = [ - [ - 'action' => '/oldest', - 'inputs' => [ 'test_input' ], - 'args' => $args, - ], - ]; - $expected = [ - '/newest' => $action_forms, - '/oldest' => $action_forms, - ]; - - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - $max_size = strlen( serialize( $expected ) ); - - FunctionMocker::replace( - 'constant', - static function ( $name ) use ( $day_in_seconds ) { - return 'DAY_IN_SECONDS' === $name ? $day_in_seconds : 0; - } - ); - WP_Mock::userFunction( 'wp_parse_args' )->andReturnUsing( - static function ( $values, $defaults ) { - return array_merge( $defaults, $values ); - } - ); - WP_Mock::userFunction( 'get_transient' ) - ->with( AutoVerify::TRANSIENT ) - ->once() - ->andReturn( $transient ); + ->andReturn( false ); WP_Mock::userFunction( 'get_option' )->andReturn( false ); - WP_Mock::userFunction( 'update_option' )->andReturn( true ); - WP_Mock::onFilter( 'hcap_auto_verify_transient_max_size' ) - ->with( AutoVerify::MAX_TRANSIENT_SIZE ) - ->reply( $max_size ); - WP_Mock::userFunction( 'set_transient' ) - ->with( AutoVerify::TRANSIENT, $expected, $day_in_seconds ) - ->once(); - - $subject = Mockery::mock( AutoVerify::class )->makePartial(); - $method = 'update_transient'; - - $subject->shouldAllowMockingProtectedMethods(); - $subject->$method( $forms_data ); - } - - /** - * Data provider for test_update_transient(). - * - * @return array - */ - public function dp_test_update_transient(): array { - $args = [ - 'action' => 'hcaptcha_action', - 'name' => 'hcaptcha_nonce', - 'auto' => true, - ]; - $test_forms_data = [ - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input' ], - 'args' => $args, - ], - ]; - $test_transient = [ - '/autoverify' => + WP_Mock::userFunction( 'update_option' ) + ->with( + 'hcaptcha_auto_verify_form_' . hash( 'sha256', '/autoverify' ), [ [ 'inputs' => [ 'test_input' ], - 'args' => $args, + 'args' => [ 'auto' => true ], ], ], - ]; + false + ) + ->once(); + WP_Mock::userFunction( 'set_transient' )->never(); + WP_Mock::userFunction( 'delete_transient' )->never(); - return [ - 'Empty transient and forms_data' => [ - 'transient' => false, - 'forms_data' => [], - 'expected' => [], - ], - 'Empty forms_data' => [ - 'transient' => $test_transient, - 'forms_data' => [], - 'expected' => $test_transient, - ], - 'Add new form' => [ - 'transient' => [], - 'forms_data' => $test_forms_data, - 'expected' => $test_transient, - ], - 'Add form with multiple inputs' => [ - 'transient' => [], - 'forms_data' => [ - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input', 'test_input2' ], - 'args' => $args, - ], - ], - 'expected' => [ - '/autoverify' => [ - [ - 'inputs' => [ 'test_input', 'test_input2' ], - 'args' => $args, - ], - ], - ], - ], - 'Add forms with same action' => [ - 'transient' => $test_transient, - 'forms_data' => [ - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input' ], - 'args' => $args, - ], - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input1', 'test_input2' ], - 'args' => $args, - ], - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input3', 'test_input4' ], - 'args' => $args, - ], - ], - 'expected' => [ - '/autoverify' => [ - [ - 'inputs' => [ 'test_input' ], - 'args' => $args, - ], - [ - 'inputs' => [ 'test_input1', 'test_input2' ], - 'args' => $args, - ], - [ - 'inputs' => [ 'test_input3', 'test_input4' ], - 'args' => $args, - ], - ], - ], - ], - 'Preserve forms with different widgets' => [ - 'transient' => [], - 'forms_data' => [ - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input' ], - 'widget_id' => 'first-widget-id', - 'args' => $args, - ], - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input' ], - 'widget_id' => 'second-widget-id', - 'args' => $args, - ], - ], - 'expected' => [ - '/autoverify' => [ - [ - 'inputs' => [ 'test_input' ], - 'args' => $args, - 'widget_id' => 'first-widget-id', - ], - [ - 'inputs' => [ 'test_input' ], - 'args' => $args, - 'widget_id' => 'second-widget-id', - ], - ], - ], - ], - 'Add forms with different actions' => [ - 'transient' => $test_transient, - 'forms_data' => [ - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input', 'test_input2' ], - 'args' => $args, - ], - [ - 'action' => '/autoverify2', - 'inputs' => [ 'test_input3', 'test_input4' ], - 'args' => $args, - ], - ], - 'expected' => [ - '/autoverify' => [ - [ - 'inputs' => [ 'test_input' ], - 'args' => $args, - ], - [ - 'inputs' => [ 'test_input', 'test_input2' ], - 'args' => $args, - ], - ], - '/autoverify2' => [ - [ - 'inputs' => [ 'test_input3', 'test_input4' ], - 'args' => $args, - ], - ], - ], - ], - 'Remove form' => [ - 'transient' => $test_transient, - 'forms_data' => [ - [ - 'action' => '/autoverify', - 'inputs' => [ 'test_input' ], - 'args' => [ 'auto' => false ], - ], - [ - 'action' => '/autoverify2', - 'inputs' => [ 'test_input3', 'test_input4' ], - 'args' => $args, - ], - ], - 'expected' => [ - '/autoverify2' => [ - [ - 'inputs' => [ 'test_input3', 'test_input4' ], - 'args' => $args, - ], - ], - ], - ], - ]; + $subject = Mockery::mock( AutoVerify::class )->makePartial(); + $subject->shouldAllowMockingProtectedMethods(); + $subject->update_form_registrations( [ $data ] ); } /** diff --git a/yarn.lock b/yarn.lock index 9db8d46cc..0cdfc23aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -60,8 +60,8 @@ __metadata: linkType: hard "@babel/eslint-parser@npm:^7.28.6": - version: 7.29.7 - resolution: "@babel/eslint-parser@npm:7.29.7" + version: 7.29.9 + resolution: "@babel/eslint-parser@npm:7.29.9" dependencies: "@nicolo-ribaudo/eslint-scope-5-internals": "npm:5.1.1-v1" eslint-visitor-keys: "npm:^2.1.0" @@ -69,7 +69,7 @@ __metadata: peerDependencies: "@babel/core": ^7.11.0 eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - checksum: 10/fb12b127ba7231b07eb4a809fd7cf07f9df6f1abe5f9a0ce8be5edfce07c5a9f6f5745cd608af19b037961e4e8d5e30f844c7fa5f7b3d6d24f017f2194156a50 + checksum: 10/a3bf296a1fa57211ea3f291a014849e09db2b0119a0b13e329d559d891d5d15af6ec3ee2fb6f94b6c3e9626c3a7752ea16bfa8dd2f319893f996f9b7c46d007d languageName: node linkType: hard @@ -288,13 +288,13 @@ __metadata: linkType: hard "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.29.7, @babel/parser@npm:^7.29.8": - version: 7.29.8 - resolution: "@babel/parser@npm:7.29.8" + version: 7.29.9 + resolution: "@babel/parser@npm:7.29.9" dependencies: "@babel/types": "npm:^7.29.8" bin: parser: ./bin/babel-parser.js - checksum: 10/dbd14ebbba0fa3019acd2712b0db3ffd594d0850d4fc487667287b5702893f54a7911570ea842f0cff4dc492a2412e3287dfabfe00c6b78efa7becb1255f3f86 + checksum: 10/b8d6acda3fc7405c497b839b0b6d46cbd309d81bdb310c349e5760ecd689c3641925e1184574106161ce2c78cf5c09d22544a0b3e4d5f002e3b78c263c6f11e6 languageName: node linkType: hard @@ -1174,8 +1174,8 @@ __metadata: linkType: hard "@babel/plugin-transform-typescript@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/plugin-transform-typescript@npm:7.29.7" + version: 7.29.9 + resolution: "@babel/plugin-transform-typescript@npm:7.29.9" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.29.7" "@babel/helper-create-class-features-plugin": "npm:^7.29.7" @@ -1184,7 +1184,7 @@ __metadata: "@babel/plugin-syntax-typescript": "npm:^7.29.7" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/88d39bcdfee418795ad65097fff88f491e87793b958fe334273a20f74693b64369456a6d666eb0f1e98c9ecb633c99bcccf90562593e64eb92cc9dd040f25c1c + checksum: 10/b80da0cf3e36965d5f94ee103b8942f33ad38db60d0f112a6b48dc2355b482c14c094a57f75e99d1cc1ace64eaf979dbedec978e64c5166f020c8be552c1a515 languageName: node linkType: hard @@ -1511,14 +1511,14 @@ __metadata: linkType: hard "@eslint-community/eslint-plugin-eslint-comments@npm:^4.7.0": - version: 4.8.0 - resolution: "@eslint-community/eslint-plugin-eslint-comments@npm:4.8.0" + version: 4.8.1 + resolution: "@eslint-community/eslint-plugin-eslint-comments@npm:4.8.1" dependencies: escape-string-regexp: "npm:^4.0.0" ignore: "npm:^7.0.5" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - checksum: 10/9110febc786c0b603b4efc5a73a5ba30bb4e853fd52f03ec7d1fed69a66a66f8633566edffaab314c2daeb4f25db5c1126f11d38cd6cd68012def729b0588a6a + checksum: 10/7a211197288992c5bd553e7158c198dc96deaa6a41063c602adedc77f40c04da0b0050a01c357ab98b0cb1b21b33f25c0195cf796b72d8bbab7cce6ec8ac9507 languageName: node linkType: hard @@ -1694,9 +1694,9 @@ __metadata: languageName: node linkType: hard -"@jest/console@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/console@npm:30.5.1" +"@jest/console@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/console@npm:30.5.2" dependencies: "@jest/types": "npm:30.5.1" "@types/node": "npm:*" @@ -1704,19 +1704,19 @@ __metadata: jest-message-util: "npm:30.5.1" jest-util: "npm:30.5.1" slash: "npm:^3.0.0" - checksum: 10/771393840ad22d9f024fc257b3f27eaba29f66d9da2b64f276e2416717e32ae1588b63142ac4487d9c2fc15364a4a7cf82824edd70d89c74705247e9e87225c5 + checksum: 10/2dea07a84013f9642dbb07b88b6278057d5dab9e2dc172c905434e7917baffd4af5a0a63a5b45f03f9065171f6ed176091ac78019f6f2c06d95d78ecb98194bd languageName: node linkType: hard -"@jest/core@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/core@npm:30.5.1" +"@jest/core@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/core@npm:30.5.2" dependencies: - "@jest/console": "npm:30.5.1" + "@jest/console": "npm:30.5.2" "@jest/pattern": "npm:30.5.0" - "@jest/reporters": "npm:30.5.1" - "@jest/test-result": "npm:30.5.1" - "@jest/transform": "npm:30.5.1" + "@jest/reporters": "npm:30.5.2" + "@jest/test-result": "npm:30.5.2" + "@jest/transform": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" ansi-escapes: "npm:^4.3.2" @@ -1726,18 +1726,18 @@ __metadata: fast-json-stable-stringify: "npm:^2.1.0" graceful-fs: "npm:^4.2.11" jest-changed-files: "npm:30.5.1" - jest-config: "npm:30.5.1" + jest-config: "npm:30.5.2" jest-haste-map: "npm:30.5.1" jest-message-util: "npm:30.5.1" jest-regex-util: "npm:30.5.0" jest-resolve: "npm:30.5.1" - jest-resolve-dependencies: "npm:30.5.1" - jest-runner: "npm:30.5.1" - jest-runtime: "npm:30.5.1" - jest-snapshot: "npm:30.5.1" + jest-resolve-dependencies: "npm:30.5.2" + jest-runner: "npm:30.5.2" + jest-runtime: "npm:30.5.2" + jest-snapshot: "npm:30.5.2" jest-util: "npm:30.5.1" jest-validate: "npm:30.5.1" - jest-watcher: "npm:30.5.1" + jest-watcher: "npm:30.5.2" pretty-format: "npm:30.5.1" slash: "npm:^3.0.0" peerDependencies: @@ -1745,7 +1745,7 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: 10/7e16c9d7b6d65edd93428a89fc384b50dd46338faee0c952593458f3098e7e7fb6ff8a22ed935e5c5414c48d8d0f01111efac1ebefdadb5b537c49ddac9ee686 + checksum: 10/2a7b17651410feae891ca7aa54b017de45dcaf52d02b93dc68b77c2cc56f97b9a2eedd878d5ed1d1ef6aebcdc9f13d272ec19705077ab531a9a17a8fbe353dfa languageName: node linkType: hard @@ -1756,15 +1756,15 @@ __metadata: languageName: node linkType: hard -"@jest/environment-jsdom-abstract@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/environment-jsdom-abstract@npm:30.5.1" +"@jest/environment-jsdom-abstract@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/environment-jsdom-abstract@npm:30.5.2" dependencies: - "@jest/environment": "npm:30.5.1" - "@jest/fake-timers": "npm:30.5.1" + "@jest/environment": "npm:30.5.2" + "@jest/fake-timers": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" - jest-mock: "npm:30.5.1" + jest-mock: "npm:30.5.2" jest-util: "npm:30.5.1" peerDependencies: "@types/jsdom": "*" @@ -1773,52 +1773,52 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10/824da6bab186279c8349af3b44cb1d45d31a0331b05ba1d41edba9ffeca446fef9a69bf6d1a67dcd7c41d32579b01e3cc8e9b8601f06b1c6149433b16cb4437c + checksum: 10/fafe9b4ea6131fc1ce4abd3caeec84f91671a48bfa015820c6b4fe3814912274d64b7282ac7a8859755a261c88006d324fd54aab3731caad28edb8da5ebdaa64 languageName: node linkType: hard -"@jest/environment@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/environment@npm:30.5.1" +"@jest/environment@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/environment@npm:30.5.2" dependencies: - "@jest/fake-timers": "npm:30.5.1" + "@jest/fake-timers": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" - jest-mock: "npm:30.5.1" - checksum: 10/19be99eeddfbfd33aa0cd065bd410c4095bea4747972b26eb0845c1e9a09989f383f819c742a5872bcba799d23bfcdc4547f88002cf13f1d473e846eab0ac5e1 + jest-mock: "npm:30.5.2" + checksum: 10/c033375dec4c6bacb4ad6ebb6602ace7d87d8c674e4f25aa729a33bc469306105d760b7d7d2a63363b039b364f7e85989e604123c20bc52a32eba302e16cdb9c languageName: node linkType: hard -"@jest/expect-utils@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/expect-utils@npm:30.5.1" +"@jest/expect-utils@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/expect-utils@npm:30.5.2" dependencies: "@jest/get-type": "npm:30.5.0" - checksum: 10/6b062302e9d3d2a948321e36a51cbe4f498f4cfb4875997d2a92a7662e8987e54c2cc0c5a9e2e4424212cb038bfd0cec82fe564653b1c62225f1633c9f00cd3e + checksum: 10/ca45ae83d0901241e7b561164719c20daae7e23fb7433f7d7c6762d5f825e61dcce062a6191972b00ef72c6d49be6ce2c8a227aeb8777df8dbcca02177d08935 languageName: node linkType: hard -"@jest/expect@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/expect@npm:30.5.1" +"@jest/expect@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/expect@npm:30.5.2" dependencies: - expect: "npm:30.5.1" - jest-snapshot: "npm:30.5.1" - checksum: 10/6dd2ae3f73a2f85b475d165c964ec46e2bfb2469b72115d76ce4a80ab01b4aa85392f57b013ee7b3c7adbc59859add27e567252de22509840157763c609a9b30 + expect: "npm:30.5.2" + jest-snapshot: "npm:30.5.2" + checksum: 10/5d05a885ce180ed007eed09dcb9308a78ea58f6a8924cc1cab0426512abb8ccfe7ed735e3a2307f425460dcd95b67692d2590cbbdebfa84049767b43ba72f2f7 languageName: node linkType: hard -"@jest/fake-timers@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/fake-timers@npm:30.5.1" +"@jest/fake-timers@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/fake-timers@npm:30.5.2" dependencies: "@jest/types": "npm:30.5.1" "@sinonjs/fake-timers": "npm:^15.4.0" "@types/node": "npm:*" jest-message-util: "npm:30.5.1" - jest-mock: "npm:30.5.1" + jest-mock: "npm:30.5.2" jest-util: "npm:30.5.1" - checksum: 10/bf83dd689d266496e290883c791b33d9731ad1b40bec056b619a2dda5a48bb32af9a42dcd280d08824000bfab8eaf86a6ef31b4cc54a001b364b5bd2b0b8b052 + checksum: 10/53a8a093e4191f93c9184fb94820afb062c503dd362c9bd74c785b4c62aad3a10b6a0ab44f258c28125039bbcd5b102aa3006ee5ff39e6f053b903398f6785cc languageName: node linkType: hard @@ -1829,15 +1829,15 @@ __metadata: languageName: node linkType: hard -"@jest/globals@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/globals@npm:30.5.1" +"@jest/globals@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/globals@npm:30.5.2" dependencies: - "@jest/environment": "npm:30.5.1" - "@jest/expect": "npm:30.5.1" + "@jest/environment": "npm:30.5.2" + "@jest/expect": "npm:30.5.2" "@jest/types": "npm:30.5.1" - jest-mock: "npm:30.5.1" - checksum: 10/19e5d3ec4a51f2fe336f6d4da392ad707ef8cc67017e9a3ef202658c6c7715ec6756c797210e47138f347b96524f832b9a8d5cf4750ab2de2e26fd9131c0eebf + jest-mock: "npm:30.5.2" + checksum: 10/664efe47ae27baf20e1d99e71e44628c0c15b331f55eba66f158a14b51a6a93c20a1e8c190e681ec377d6e98442f3a4855191c599ad156a79cab8cc6c82fda9c languageName: node linkType: hard @@ -1859,20 +1859,20 @@ __metadata: linkType: hard "@jest/react-is-19@npm:react-is@^19.2.5": - version: 19.2.8 - resolution: "react-is@npm:19.2.8" - checksum: 10/09d053ff36e0ba4d47164baf9eab1c7772791f0371aa92281f0e547e0f6a5a5c8a1eb939aadd1a08b3c565958dd63ee59c746b6ac0d5ccb14ad742ac0314ab15 + version: 19.3.0 + resolution: "react-is@npm:19.3.0" + checksum: 10/69cfafcab48caac03da35fb17e640a2a2de8299a3cd699da720e671a22231fc7179c77c82e379826728857f9cc45cb56ff1e7f5d607acdd48fe151417a2a388f languageName: node linkType: hard -"@jest/reporters@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/reporters@npm:30.5.1" +"@jest/reporters@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/reporters@npm:30.5.2" dependencies: "@bcoe/v8-coverage": "npm:^0.2.3" - "@jest/console": "npm:30.5.1" - "@jest/test-result": "npm:30.5.1" - "@jest/transform": "npm:30.5.1" + "@jest/console": "npm:30.5.2" + "@jest/test-result": "npm:30.5.2" + "@jest/transform": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@jridgewell/trace-mapping": "npm:^0.3.31" "@types/node": "npm:*" @@ -1897,7 +1897,7 @@ __metadata: peerDependenciesMeta: node-notifier: optional: true - checksum: 10/16ec1eeb921a2fc31a28c4482036e2895bc97c7bd6ae511f55e8d20361cdea5138d5851aaf5cc721d4133deac19a8c64a8706498c2e8bf7fb766d65286bb5e13 + checksum: 10/b5221fb70ae0a379fb8a4dcc593d38004655a5d5e6d20fccac5d717b143503af07c39a4f5007878270cde44373046b9857755151fa03efeb0d6859d3ad3dab81 languageName: node linkType: hard @@ -1922,45 +1922,45 @@ __metadata: languageName: node linkType: hard -"@jest/source-map@npm:30.5.0": - version: 30.5.0 - resolution: "@jest/source-map@npm:30.5.0" +"@jest/source-map@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/source-map@npm:30.5.2" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.31" callsites: "npm:^3.1.0" convert-source-map: "npm:^2.0.0" graceful-fs: "npm:^4.2.11" - checksum: 10/404461bca00329c7e9a945cb92e38f7d8c57296b1b91461bae55db0a44ab9965ea6981cd54b46142b71fed863affa935d72a0df53f71a18dcdfddcb676de1daf + checksum: 10/c0d76d8537cd167d2c7d43b486254dd2172bfd3c939996e2decf3e34c77cfa38536ddd2de6a315a88b9d08066b18c8324e2b19e6301cebc033ddb3a6b1f8bd0b languageName: node linkType: hard -"@jest/test-result@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/test-result@npm:30.5.1" +"@jest/test-result@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/test-result@npm:30.5.2" dependencies: - "@jest/console": "npm:30.5.1" + "@jest/console": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/istanbul-lib-coverage": "npm:^2.0.6" collect-v8-coverage: "npm:^1.0.2" - checksum: 10/a0233f5a0ef7bf0470ce3608e43832b8029e6e6a3827ee87b5336275e6dcd776b95d7d9c7cd15b0667b42cd6a1277f43554198673d7273fb2b10d6a6319eceb6 + checksum: 10/a72b61ab6e23bf63d1eacb83ed5d0c756a8eebb41f0e4133098ecb0fc3e336f93019cac20924aa347cb25f92fefa0393d118205b57b18da4925ca1ffd1c7cd8f languageName: node linkType: hard -"@jest/test-sequencer@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/test-sequencer@npm:30.5.1" +"@jest/test-sequencer@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/test-sequencer@npm:30.5.2" dependencies: - "@jest/test-result": "npm:30.5.1" + "@jest/test-result": "npm:30.5.2" graceful-fs: "npm:^4.2.11" jest-haste-map: "npm:30.5.1" slash: "npm:^3.0.0" - checksum: 10/d0d8257b6f8912714f4972aa5a1e394120c2adfc8cb2a25255107ab44414c6aa0e777152cba0ca942707474f6e7d414c6e1358044b33e411966e4a112075baf8 + checksum: 10/f4696a7dcdc5b19e8c1f80701fc5fc3eee8b03ee516a3ccfa0192f73c0f7bd2c36b7c7e7e666afbe7b75a25744423dad7307cb3d5c8a243d438b627efd21afe8 languageName: node linkType: hard -"@jest/transform@npm:30.5.1": - version: 30.5.1 - resolution: "@jest/transform@npm:30.5.1" +"@jest/transform@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/transform@npm:30.5.2" dependencies: "@babel/core": "npm:^7.27.4" "@jest/types": "npm:30.5.1" @@ -1976,7 +1976,7 @@ __metadata: pirates: "npm:^4.0.7" slash: "npm:^3.0.0" write-file-atomic: "npm:^5.0.1" - checksum: 10/ead2db99e0f38484dd4f6b1453bb98bd0fd3254818735a456aa7877d2f7f81dad608c8d57fa04e087f96701f48d688b55b2df9c71b2efb9d81e95de447afaa74 + checksum: 10/5b2d6ce6e0c96ddec62be35c7924f6cb6550340508b63299bb0b88bef822bf9ba43e10baba6b956a0102a664060cc06cb5ae294b6b333cd94ea177c8a22b36e7 languageName: node linkType: hard @@ -2069,14 +2069,14 @@ __metadata: linkType: hard "@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.2.3 - resolution: "@napi-rs/wasm-runtime@npm:1.2.3" + version: 1.2.4 + resolution: "@napi-rs/wasm-runtime@npm:1.2.4" dependencies: "@tybys/wasm-util": "npm:^0.10.3" peerDependencies: "@emnapi/core": ^1.7.1 || ^2.0.0-alpha.4 "@emnapi/runtime": ^1.7.1 || ^2.0.0-alpha.4 - checksum: 10/86f7f092309eb005ff9e456c12a9f81ae06ff823602740a8c9c54b2dfc17620d9b4d88c4cd179541c794aed521ac57c7e37457a00e87a624867d78f6bbce0d7c + checksum: 10/5cda52f943c67eba1875d95dc7ea0dbe2c1581edb25660d3b0d780d7d87600bdaf92b4f4395e3cde82a9ccfb540da55567452e6e6d61fb13b7f51da7a3ad9151 languageName: node linkType: hard @@ -2224,13 +2224,13 @@ __metadata: linkType: hard "@peculiar/asn1-schema@npm:^2.7.0": - version: 2.9.4 - resolution: "@peculiar/asn1-schema@npm:2.9.4" + version: 2.10.0 + resolution: "@peculiar/asn1-schema@npm:2.10.0" dependencies: "@peculiar/utils": "npm:^2.0.2" asn1js: "npm:^3.0.10" tslib: "npm:^2.8.1" - checksum: 10/6f53e2856cb6cc46c8f9023712e5bd1c7f5f6feaf7da507b08e801d3e434e39de9644fbcfe9020ac261869bbf195318d5aafe19b964b685f49440eac06f04450 + checksum: 10/244569d6594b5e0dfc190930cc068085b64d2cf53359e8f801cd1661d286a565439c2e8201a2b98813bdddaa5fe79005e3e98609d3aa1f579fd4b17172839d87 languageName: node linkType: hard @@ -2352,11 +2352,11 @@ __metadata: linkType: hard "@tybys/wasm-util@npm:^0.10.3": - version: 0.10.3 - resolution: "@tybys/wasm-util@npm:0.10.3" + version: 0.10.4 + resolution: "@tybys/wasm-util@npm:0.10.4" dependencies: tslib: "npm:^2.4.0" - checksum: 10/6cf39f7a2926b1c8bc6fe3f9f03318a33dd6dae81bdbd059983f9c6ee22d10a827f12564d648c05a2d4926e03c86cbe2799fb20609ee65e9efc39603039b4765 + checksum: 10/da910a0439afad49a6427e4d16fcd5a52199cb471e07c262e1dd5ed7664908e6f142f4dca1b09895c53f4ca4402663042d760f41c9fa7ff33f76c662739646df languageName: node linkType: hard @@ -2451,7 +2451,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -2473,11 +2473,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 26.5.0 - resolution: "@types/node@npm:26.5.0" + version: 26.6.3 + resolution: "@types/node@npm:26.6.3" dependencies: undici-types: "npm:~8.9.0" - checksum: 10/db4b0ac5289abaf7fa6f280610853a3a9c88cd60ef686513fb773badf77bbf6089cafc9522bc99d0765f13b43cde3109b37a8183859a2adeec00ae07f0a4710d + checksum: 10/278d5241522520fbadd46b41065d7587bbb214adb3c6f8a2a9a28754d374c578d090f7387866d09c79be1e9bbf5adc7eeea453032a52f77e70b06077f85c5c14 languageName: node linkType: hard @@ -2518,105 +2518,105 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.70.0" +"@typescript-eslint/eslint-plugin@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.71.0" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.70.0" - "@typescript-eslint/type-utils": "npm:8.70.0" - "@typescript-eslint/utils": "npm:8.70.0" - "@typescript-eslint/visitor-keys": "npm:8.70.0" + "@typescript-eslint/scope-manager": "npm:8.71.0" + "@typescript-eslint/type-utils": "npm:8.71.0" + "@typescript-eslint/utils": "npm:8.71.0" + "@typescript-eslint/visitor-keys": "npm:8.71.0" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.5.0" peerDependencies: - "@typescript-eslint/parser": ^8.70.0 + "@typescript-eslint/parser": ^8.71.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/d078f6d7212b9f6d83f8240d4458d10a9975eaab74ab09b2ed3a1000fe3d94ba5c510dded243fd8750bd7fdbf58e63742ac29c996e737234430ab48910389ab1 + checksum: 10/3b4c24bd7ceb5fd01d073e111f1108cada55c07d723cc81231c84a5767dae8b6abc8d734810a28c7987c5b7fd69b168ec743482a1715945e9aaceb2ef8e23b39 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/parser@npm:8.70.0" +"@typescript-eslint/parser@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/parser@npm:8.71.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.70.0" - "@typescript-eslint/types": "npm:8.70.0" - "@typescript-eslint/typescript-estree": "npm:8.70.0" - "@typescript-eslint/visitor-keys": "npm:8.70.0" + "@typescript-eslint/scope-manager": "npm:8.71.0" + "@typescript-eslint/types": "npm:8.71.0" + "@typescript-eslint/typescript-estree": "npm:8.71.0" + "@typescript-eslint/visitor-keys": "npm:8.71.0" debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/44ea118c7a0c0d4e8ce21845784cda4727240e10a1e8966f759fc48363748a48a74a0aa228664a2148fa24f586d77e230d98058916f8a1a1c85325bb1f9d2e0f + checksum: 10/198b78dd5f9ccd14f8f4f90467c5c9ab9cae1769ec32d8e9fb3008269bdf62373a8913951c2fe66ed3b981e16a95947736aa5a18f37ffa3612845c6f31f29187 languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/project-service@npm:8.70.0" +"@typescript-eslint/project-service@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/project-service@npm:8.71.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.70.0" - "@typescript-eslint/types": "npm:^8.70.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.71.0" + "@typescript-eslint/types": "npm:^8.71.0" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/b06286f1ec6fc0fde7382f14cf4645aa0d008dfc25f40a8ec58e644fd42058a91e1d9ce1e106fbe78f5c2a3cf6b75560a24d24901972a4d21cc604f13aa921ad + checksum: 10/7cf26e65414b08e4a503635d40104b8c495059861428648b78715af780415b455454aec02f444027dfe3afb4e8850b6f65f6c2f03f6d41811e13056d06443234 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/scope-manager@npm:8.70.0" +"@typescript-eslint/scope-manager@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/scope-manager@npm:8.71.0" dependencies: - "@typescript-eslint/types": "npm:8.70.0" - "@typescript-eslint/visitor-keys": "npm:8.70.0" - checksum: 10/7168d1ddf945ee47179420063bcc867f289926c7bea348bab4b9310b7c9b0f4b9031eb7653a820bb9fe41f668cc724c9886e19b7e39e2220ed0df168e8cf7607 + "@typescript-eslint/types": "npm:8.71.0" + "@typescript-eslint/visitor-keys": "npm:8.71.0" + checksum: 10/671d2dd893cfd657f20190caf570b6aa9bb39d2b0ba19ac1937180c2981b00af76edbbfab24f445c71f2abba87a762e3e49a22a5ade318a8c6f30e006966c648 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.70.0, @typescript-eslint/tsconfig-utils@npm:^8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.70.0" +"@typescript-eslint/tsconfig-utils@npm:8.71.0, @typescript-eslint/tsconfig-utils@npm:^8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.71.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/c4ad046c1082ebca815d52fd9ababe122ba8c59012ae69e790185ea0e2f898fe5abc2b5256ff88ee167d19844a5f6152dc63342e0a866cfe22a11c2887c5d8da + checksum: 10/1881f383fd3e34e4a9a63d315a26ebbe4faa8528d225539035fd32ee37f5055e80bf6f28fe423e4d62e3ea5e4e5b2319253583e95d41f0c66ac95943e752b588 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/type-utils@npm:8.70.0" +"@typescript-eslint/type-utils@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/type-utils@npm:8.71.0" dependencies: - "@typescript-eslint/types": "npm:8.70.0" - "@typescript-eslint/typescript-estree": "npm:8.70.0" - "@typescript-eslint/utils": "npm:8.70.0" + "@typescript-eslint/types": "npm:8.71.0" + "@typescript-eslint/typescript-estree": "npm:8.71.0" + "@typescript-eslint/utils": "npm:8.71.0" debug: "npm:^4.4.3" ts-api-utils: "npm:^2.5.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/54bf0ef53a67f3e58bcbbefa1837d6b973d31e6c682ce6447c863426fc396fbcac2800dc7e2d343d09029e5c333668f29463ec99197562ee6211736c8460e4fe + checksum: 10/3d7fa4164e1b1e83dc83b23a0cba6570f615cac8d82155429c27fcf26aa745c107aa7fc91264050512c0cbdf5cf8b491abf9528e5d4bbed13731c75a75778ee8 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.70.0, @typescript-eslint/types@npm:^8.11.0, @typescript-eslint/types@npm:^8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/types@npm:8.70.0" - checksum: 10/a264fbebf12d3ee5363688f10713890f67264277749c7f7e2b7fa8636d8836a19a9532e7228bd3155b12f455121b22998282acb17e76561e81902891f7205b44 +"@typescript-eslint/types@npm:8.71.0, @typescript-eslint/types@npm:^8.11.0, @typescript-eslint/types@npm:^8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/types@npm:8.71.0" + checksum: 10/24c72ad99a5f3edb0021f87cd1366f63a73ecdd537b3c89d8189c52b999499a498913eed17c46edacca1e3990fd761e28293d2cf3cfde0a1f637d1879f1cb10f languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.70.0" +"@typescript-eslint/typescript-estree@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.71.0" dependencies: - "@typescript-eslint/project-service": "npm:8.70.0" - "@typescript-eslint/tsconfig-utils": "npm:8.70.0" - "@typescript-eslint/types": "npm:8.70.0" - "@typescript-eslint/visitor-keys": "npm:8.70.0" + "@typescript-eslint/project-service": "npm:8.71.0" + "@typescript-eslint/tsconfig-utils": "npm:8.71.0" + "@typescript-eslint/types": "npm:8.71.0" + "@typescript-eslint/visitor-keys": "npm:8.71.0" debug: "npm:^4.4.3" minimatch: "npm:^10.2.2" semver: "npm:^7.7.3" @@ -2624,32 +2624,32 @@ __metadata: ts-api-utils: "npm:^2.5.0" peerDependencies: typescript: ">=4.8.4 <6.1.0" - checksum: 10/c588aac52c24883befe0c07312df808f27fb00676250e20036f43760cbe831e482ec92232703d9dea95c583687cf374a23e2c49cf8afddce370929eb128270f4 + checksum: 10/6d1022ba1ca4da2083078f3c654e4e3faacf43bcd91f7f2a62320a45c20613e67deaf4de04c4b4656446bf7289d4f1152f966afcf9fcbbaa12357dff3bc039b4 languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.70.0, @typescript-eslint/utils@npm:^8.0.0": - version: 8.70.0 - resolution: "@typescript-eslint/utils@npm:8.70.0" +"@typescript-eslint/utils@npm:8.71.0, @typescript-eslint/utils@npm:^8.0.0": + version: 8.71.0 + resolution: "@typescript-eslint/utils@npm:8.71.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.70.0" - "@typescript-eslint/types": "npm:8.70.0" - "@typescript-eslint/typescript-estree": "npm:8.70.0" + "@typescript-eslint/scope-manager": "npm:8.71.0" + "@typescript-eslint/types": "npm:8.71.0" + "@typescript-eslint/typescript-estree": "npm:8.71.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/518521b1f7e8bb7a5bd7bc2db39e85b3bd1d710d0cf92da9a7d13a3394e9fb99eda76b90b7ecdc334afc60cb4415b8e079439a3cb9b29b6aea7f8ff1f499590b + checksum: 10/ace4ff09bb7a3592dc83c2a56de48a6e8631615fba2060eb38b721c40e8e0b532869547f33c718ee761039df970e607126d873716bf23e4b2a877e4185e56e4d languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.70.0": - version: 8.70.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.70.0" +"@typescript-eslint/visitor-keys@npm:8.71.0": + version: 8.71.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.71.0" dependencies: - "@typescript-eslint/types": "npm:8.70.0" + "@typescript-eslint/types": "npm:8.71.0" eslint-visitor-keys: "npm:^5.0.0" - checksum: 10/3b6724e29e68f4468da18aaf7300dd178d5aa0465e6f3a203f91920d3bda6d0cdb97615ada9d1b04a63ccb485301cc4cd701e78931061ec2ec90a3f008cba3c7 + checksum: 10/48ced48cf4d051fd9a3e1a6c10ebbca593ba5619553657e1100e17a60081bcee2a7bb9b11431b95f52478572bdd2e5be0f044678a570d9ce50b45c9bbdf5ea7e languageName: node linkType: hard @@ -2970,8 +2970,8 @@ __metadata: linkType: hard "@wordpress/babel-preset-default@npm:^8.54.0": - version: 8.54.0 - resolution: "@wordpress/babel-preset-default@npm:8.54.0" + version: 8.56.0 + resolution: "@wordpress/babel-preset-default@npm:8.56.0" dependencies: "@babel/core": "npm:^7.25.7" "@babel/plugin-syntax-import-attributes": "npm:^7.26.0" @@ -2979,35 +2979,35 @@ __metadata: "@babel/plugin-transform-runtime": "npm:^7.25.7" "@babel/preset-env": "npm:^7.25.7" "@babel/preset-typescript": "npm:^7.25.7" - "@wordpress/browserslist-config": "npm:^6.54.0" - "@wordpress/warning": "npm:^3.54.0" - browserslist: "npm:^4.28.4" + "@wordpress/browserslist-config": "npm:^6.56.0" + "@wordpress/warning": "npm:^3.56.0" + browserslist: "npm:^4.28.7" core-js: "npm:^3.31.0" react: "npm:^18.3.1" - checksum: 10/4f88b016c82a0de5e4955b35bc79fff0fef147a6929380b7c810fe6ba2a87c8ee67b1b8522bde8d96a95677fbe9adda67dbe9d15598f4b49dd05c798e09bc948 + checksum: 10/bcc1f938d3ab324a3b975a5af877c3c61e49f89778ef3c7f35eeb993e2863f46e1e56f631d26b4c26e24075157344fa1d567b2dfa5506b9c31b53f335302cf18 languageName: node linkType: hard -"@wordpress/browserslist-config@npm:^6.54.0": - version: 6.54.0 - resolution: "@wordpress/browserslist-config@npm:6.54.0" - checksum: 10/2988b596f31b0d498ef0904605a1fcf3d5a6adbb502e6179150bd9daad3ca809c2245fb57ac48067e1d383c98aa4eebe20d42a28d125b6b26cb11825e9a3c2df +"@wordpress/browserslist-config@npm:^6.56.0": + version: 6.56.0 + resolution: "@wordpress/browserslist-config@npm:6.56.0" + checksum: 10/955e8b296d39bc794d69c5bccefeb976fa1e3999c026b95678d35eaea13b174d88c2ee92d5c0811ca0e10923d57caa72dd5f7fc18f15bff1e8def17357a00bdb languageName: node linkType: hard -"@wordpress/compose@npm:^8.7.0": - version: 8.7.0 - resolution: "@wordpress/compose@npm:8.7.0" +"@wordpress/compose@npm:^8.9.0": + version: 8.9.0 + resolution: "@wordpress/compose@npm:8.9.0" dependencies: "@types/mousetrap": "npm:^1.6.8" - "@wordpress/deprecated": "npm:^4.54.0" - "@wordpress/dom": "npm:^4.54.0" - "@wordpress/element": "npm:^8.6.0" - "@wordpress/is-shallow-equal": "npm:^5.54.0" - "@wordpress/keycodes": "npm:^4.54.0" - "@wordpress/priority-queue": "npm:^3.54.0" - "@wordpress/private-apis": "npm:^1.54.0" - "@wordpress/undo-manager": "npm:^1.54.0" + "@wordpress/deprecated": "npm:^4.56.0" + "@wordpress/dom": "npm:^4.56.0" + "@wordpress/element": "npm:^8.8.0" + "@wordpress/is-shallow-equal": "npm:^5.56.0" + "@wordpress/keycodes": "npm:^4.56.0" + "@wordpress/priority-queue": "npm:^3.56.0" + "@wordpress/private-apis": "npm:^1.56.0" + "@wordpress/undo-manager": "npm:^1.56.0" change-case: "npm:^4.1.2" mousetrap: "npm:^1.6.5" use-memo-one: "npm:^1.1.1" @@ -3017,34 +3017,33 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/604650c4d28ffab9297da4706848bde65fcb3d6e16927d0a6b3714a9b5003a4c2091ba2673b2b19ab06a15ab19763f609140339b96917a042bf3231c4926e7f6 + checksum: 10/868bb345ee2f3f9d197b562e0a406111941f6233a20e88e9993894a57a24811c07a336d6bd96e32acfcba566be97c9ad8edbd52c1f5fbbc5481735de6d64e871 languageName: node linkType: hard -"@wordpress/deprecated@npm:^4.54.0": - version: 4.54.0 - resolution: "@wordpress/deprecated@npm:4.54.0" +"@wordpress/deprecated@npm:^4.56.0": + version: 4.56.0 + resolution: "@wordpress/deprecated@npm:4.56.0" dependencies: - "@wordpress/hooks": "npm:^4.54.0" - checksum: 10/8e8ecc3bdc3ab764af9729d88931502af02ddf0f494e541a361fe4ea5329a98c167aa978d3e43055cf7fcf8e26b23a29a7518608593b4f6b66cccba927fe568b + "@wordpress/hooks": "npm:^4.56.0" + checksum: 10/1ddf7bafaed599e709a59011ef3b09e3c1251c6fdd02299fd5897ffa767f160d56d7af6e052ff8ff2b4a80a4c4564985c7c061f2db0d1ebff8ee89fabe3bb29e languageName: node linkType: hard -"@wordpress/dom@npm:^4.54.0": - version: 4.54.0 - resolution: "@wordpress/dom@npm:4.54.0" +"@wordpress/dom@npm:^4.56.0": + version: 4.56.0 + resolution: "@wordpress/dom@npm:4.56.0" dependencies: - "@wordpress/deprecated": "npm:^4.54.0" - checksum: 10/ad8af545cc5150dded9600da6c9732c33c651b01adab435e9706b69bb34e2c8e5ec66192e70ee1a872160c77f92d42d0835588d89c6081de5ec42ce91b5d2991 + "@wordpress/deprecated": "npm:^4.56.0" + checksum: 10/168e454f22268e0e68edaa5f93794602b679960bd0d25ef7d1356fa047180f71af4370a0b96a8783ac6e0a35fbaf76aa6fb3b05ba3f7637742e5c44de49b44bf languageName: node linkType: hard -"@wordpress/element@npm:^8.6.0": - version: 8.6.0 - resolution: "@wordpress/element@npm:8.6.0" +"@wordpress/element@npm:^8.8.0": + version: 8.8.0 + resolution: "@wordpress/element@npm:8.8.0" dependencies: - "@wordpress/deprecated": "npm:^4.54.0" - "@wordpress/escape-html": "npm:^3.54.0" + "@wordpress/escape-html": "npm:^3.56.0" change-case: "npm:^4.1.2" is-plain-object: "npm:^5.1.0" peerDependencies: @@ -3057,14 +3056,14 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10/8c0709e0be1efa16bad7e4f795b6d9c5a81294d953abe8d1c92353b005456dadf6308178f9ac8a6ed4c93ad09d978045b181d316ced32ee4e56f04a4fdd4bcb4 + checksum: 10/ab2f35c48d5097b25ec2dbbd3ed1e7acadc9c1e70129edab200829d6c655f03def151996e30e0913734d66249ca53cf4315d39f4a756c80d92407b664c1c8fc5 languageName: node linkType: hard -"@wordpress/escape-html@npm:^3.54.0": - version: 3.54.0 - resolution: "@wordpress/escape-html@npm:3.54.0" - checksum: 10/ab735a898f5d4fee152a34da1bab6d8250954338fb17a169024229de0628095cd61d1fa82decd79bbf8f382d6fb9be2c6fec9466b2e024b7f419321163273218 +"@wordpress/escape-html@npm:^3.56.0": + version: 3.56.0 + resolution: "@wordpress/escape-html@npm:3.56.0" + checksum: 10/ebda733884d7c3c46f92689a29a416dcf3882ebe347c027e8a5433272a48f39e7fe144a5a222904c8251de7d8a0f2cf74b13b31358456a2ade56a589a31e8d03 languageName: node linkType: hard @@ -3106,94 +3105,95 @@ __metadata: languageName: node linkType: hard -"@wordpress/hooks@npm:^4.52.0, @wordpress/hooks@npm:^4.54.0": - version: 4.54.0 - resolution: "@wordpress/hooks@npm:4.54.0" - checksum: 10/c2b5054f0ad1a3171a8e55899d7a29c79a1a1542f86fe6d39948a4ff7f07a6fc06bee1df3dc8126def7ba2ad47b0de49db9505a3a5196d40f394b2b63e1379d2 +"@wordpress/hooks@npm:^4.52.0, @wordpress/hooks@npm:^4.56.0": + version: 4.56.0 + resolution: "@wordpress/hooks@npm:4.56.0" + checksum: 10/1cfb2bf817bd0c930c97ba48bbb1420386cdd8a80a0534b7b8b7f6bbe2c1593eba8e03f7cffc808893d4a17fb8c01ae2b8768e73ab90c73466a8b6617c5278c6 languageName: node linkType: hard -"@wordpress/i18n@npm:^6.27.0": - version: 6.27.0 - resolution: "@wordpress/i18n@npm:6.27.0" +"@wordpress/i18n@npm:^6.29.0": + version: 6.29.0 + resolution: "@wordpress/i18n@npm:6.29.0" dependencies: "@tannin/sprintf": "npm:^1.3.2" - "@wordpress/hooks": "npm:^4.54.0" + "@wordpress/hooks": "npm:^4.56.0" gettext-parser: "npm:^1.3.1" tannin: "npm:^1.2.0" bin: pot-to-php: tools/pot-to-php.js - checksum: 10/07755b9337feff5b9e26a89f403e29f5aa7467e96453348234809b648938321f2b9db837cb2d49ddef3c5200bd094f2c5f7423ffcb80bb8b0937f59f63d4c20f + checksum: 10/00ff031e9e36a0adfa26140e8fb871906fd869ee1c4a84b3cd977a0a088f1739cbe0b8751c44213db71485e1e914bcba2387e9d829b620cd1e8099f501bde088 languageName: node linkType: hard -"@wordpress/is-shallow-equal@npm:^5.54.0": - version: 5.54.0 - resolution: "@wordpress/is-shallow-equal@npm:5.54.0" - checksum: 10/5c52d094c0e523eda715cfa8de17ede2999eb4098b92768654dc2d1e0148db7b3a7dd787664a3f538cb66a3ce325d39cce2b8b8d712fe772b2f9d12ca901e6da +"@wordpress/is-shallow-equal@npm:^5.56.0": + version: 5.56.0 + resolution: "@wordpress/is-shallow-equal@npm:5.56.0" + checksum: 10/6fbed312ed1f88990aeaf71b3b29e07ff6d98b9cf59354d7faa967e4f6b611a8c49ddf01b1021f988812e85b04b580dd0f88cf062e2676fda34c3f9a687f1a3a languageName: node linkType: hard -"@wordpress/keycodes@npm:^4.54.0": - version: 4.54.0 - resolution: "@wordpress/keycodes@npm:4.54.0" +"@wordpress/keycodes@npm:^4.56.0": + version: 4.56.0 + resolution: "@wordpress/keycodes@npm:4.56.0" dependencies: - "@wordpress/i18n": "npm:^6.27.0" + "@wordpress/i18n": "npm:^6.29.0" peerDependencies: "@types/react": ^18 || ^19 peerDependenciesMeta: "@types/react": optional: true - checksum: 10/73c22e4113c32401eb6200ed80e50dea4339476643c577d74d0713b7a1d2ec151fae74bd9482a7d7e14b26d5e5255432c3193719fed2bb3c9fbf2c523e45cf2b + checksum: 10/1d72bab1a6768d4c4749151a4c19f83eed761f3b1ef6d2a7794329eff090e0bcc24008147591277c994c86fa3bb4c2fda19aa931f1417c0dffbddcde79987672 languageName: node linkType: hard "@wordpress/prettier-config@npm:^4.54.0": - version: 4.54.0 - resolution: "@wordpress/prettier-config@npm:4.54.0" + version: 4.56.0 + resolution: "@wordpress/prettier-config@npm:4.56.0" peerDependencies: prettier: ">=3" - checksum: 10/3ad1fa0168ebdd4eb019c9eb58537a255d89a20508f879c0933ffb7d5b8b1850f39216fcd218d20bed7e95181384f208756755fc611103b647e8db72f2c037eb + checksum: 10/f2f0d861822fd6b0dec5823957f14782255ecab5e3f4a6d9a51d4b09235e80835a39de24d38b563e9326b923ab1a4f8ea9c238b826d17674d161999b3645a45f languageName: node linkType: hard -"@wordpress/priority-queue@npm:^3.54.0": - version: 3.54.0 - resolution: "@wordpress/priority-queue@npm:3.54.0" +"@wordpress/priority-queue@npm:^3.56.0": + version: 3.56.0 + resolution: "@wordpress/priority-queue@npm:3.56.0" dependencies: requestidlecallback: "npm:^0.3.0" - checksum: 10/937ce7291439aedfc764fa97c95ef51c8defac01e0686e3cbe42b3ff7be321af9e3eb3d8629552e65326717de058a5335a58e88590843a3d3dd957e7788b2eda + checksum: 10/483caf43ede4fe64e1be59794fe78d203c6adb25f032079697487ac9c09c7415d80fa44be9da12e6d73f9f8d16f33167710fae6e893fd89c781560dc959c35d4 languageName: node linkType: hard -"@wordpress/private-apis@npm:^1.54.0": - version: 1.54.0 - resolution: "@wordpress/private-apis@npm:1.54.0" - checksum: 10/bf1e5a9bf1e84c5e439d4ab8ee71c18d20c05d04954e18d195a8335d6aed722e625e64f591b46b13504abd7c74f4220455134739d5e2b33985bd78a6d81226ee +"@wordpress/private-apis@npm:^1.56.0": + version: 1.56.0 + resolution: "@wordpress/private-apis@npm:1.56.0" + checksum: 10/a19dbb0d9a7b8d733ac438dec9bb93d13cdd43fe71e794563bac05d81dd22c7cdde51610b008f0445841d405ebc902d3b0bbac54ae28bfbbdff9c12baa64a789 languageName: node linkType: hard -"@wordpress/style-runtime@npm:^0.10.0": - version: 0.10.0 - resolution: "@wordpress/style-runtime@npm:0.10.0" - checksum: 10/142e8541a0d68d491b4735eb04e6cbd313cb326577b07ea84c74889d2d5207683742d530b865a82d810ca5b88dfac8ddd07c5c1aca295e3804460e1ff13a8048 +"@wordpress/style-runtime@npm:^0.12.0": + version: 0.12.0 + resolution: "@wordpress/style-runtime@npm:0.12.0" + checksum: 10/7ae2fde85e07ebb9f6ca1d27602b17c5b494b4af1d78afefbd14aee64da614c4b29c45645034dcce9cc5b709023e3ae2ed4e5146a9c6089fdfceee0b7512227c languageName: node linkType: hard "@wordpress/theme@npm:^2.0.0": - version: 2.0.0 - resolution: "@wordpress/theme@npm:2.0.0" + version: 2.2.0 + resolution: "@wordpress/theme@npm:2.2.0" dependencies: - "@wordpress/compose": "npm:^8.7.0" - "@wordpress/deprecated": "npm:^4.54.0" - "@wordpress/element": "npm:^8.6.0" - "@wordpress/private-apis": "npm:^1.54.0" - "@wordpress/style-runtime": "npm:^0.10.0" + "@wordpress/compose": "npm:^8.9.0" + "@wordpress/deprecated": "npm:^4.56.0" + "@wordpress/element": "npm:^8.8.0" + "@wordpress/private-apis": "npm:^1.56.0" + "@wordpress/style-runtime": "npm:^0.12.0" colorjs.io: "npm:^0.7.1" memize: "npm:^2.1.1" peerDependencies: "@types/react": ^18 || ^19 esbuild: ">=0.27.2 <1.0.0" + lightningcss: ">=1.33.0 <2.0.0" postcss: ^8.0.0 react: ^18 || ^19 react-dom: ^18 || ^19 @@ -3204,29 +3204,31 @@ __metadata: optional: true esbuild: optional: true + lightningcss: + optional: true postcss: optional: true stylelint: optional: true vite: optional: true - checksum: 10/3d2d5b3a891afb62987c5c07f4fa3724ed55c875d04ad0d50f79e6875dc069cba03a08c99e5f5fd18eff78d8341150da767d77265660518d9a1d5b78bca9e010 + checksum: 10/ea5cf1c79e6969882520957258661ceff729249a2e960e9e6d37e7973a1de4d47f62ac9087e8edb2cbbe3e762a84f0d46d970dd7a36ee13b137a5cf9ce7a715a languageName: node linkType: hard -"@wordpress/undo-manager@npm:^1.54.0": - version: 1.54.0 - resolution: "@wordpress/undo-manager@npm:1.54.0" +"@wordpress/undo-manager@npm:^1.56.0": + version: 1.56.0 + resolution: "@wordpress/undo-manager@npm:1.56.0" dependencies: - "@wordpress/is-shallow-equal": "npm:^5.54.0" - checksum: 10/1870b1a24f693e0a04e92c0391ee4cecef7f2baee3ad76c5ee309aecd2514037b5f807c2a208cd33f96a92e45f1b3f349eda12d3f603d114da1dc276b4a75ca0 + "@wordpress/is-shallow-equal": "npm:^5.56.0" + checksum: 10/1f6c5ba17ff1726476b79bc1666227522dd58083d86c7b1d5c8ccda60617a859ece5e76cedd0103974b7e59a56af5acf83543af88d596ef901e9e121f1e02b90 languageName: node linkType: hard -"@wordpress/warning@npm:^3.54.0": - version: 3.54.0 - resolution: "@wordpress/warning@npm:3.54.0" - checksum: 10/2a77e4e8bd68094270ad1e379a867009e93aa5cc7579b8a50e9c697869feeb8dc2644564f69ed0dc18f5b3335c1d7783d3aecd249d1861141ae82e9782418555 +"@wordpress/warning@npm:^3.56.0": + version: 3.56.0 + resolution: "@wordpress/warning@npm:3.56.0" + checksum: 10/fee7e15c5b2395bbf591bfacef1a3dd53c25131ac528000728aaef54fdd65c82a2a33e299fb237e78f2bb0191ae5668a2af006dd6ed654676df8289e8a77167d languageName: node linkType: hard @@ -3276,9 +3278,9 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:^2.1.1": - version: 2.1.1 - resolution: "ajv-formats@npm:2.1.1" +"ajv-formats@npm:^3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" dependencies: ajv: "npm:^8.0.0" peerDependencies: @@ -3286,7 +3288,7 @@ __metadata: peerDependenciesMeta: ajv: optional: true - checksum: 10/70c263ded219bf277ffd9127f793b625f10a46113b2e901e150da41931fcfd7f5592da6d66862f4449bb157ffe65867c3294a7df1d661cc232c4163d5a1718ed + checksum: 10/5679b9f9ced9d0213a202a37f3aa91efcffe59a6de1a6e3da5c873344d3c161820a1f11cc29899661fee36271fd2895dd3851b6461c902a752ad661d1c1e8722 languageName: node linkType: hard @@ -3313,7 +3315,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.9.0": +"ajv@npm:^8.0.0, ajv@npm:^8.20.0": version: 8.20.0 resolution: "ajv@npm:8.20.0" dependencies: @@ -3342,9 +3344,9 @@ __metadata: linkType: hard "ansi-regex@npm:^6.2.2": - version: 6.3.0 - resolution: "ansi-regex@npm:6.3.0" - checksum: 10/85e15deee80d6e52e75864897b6f7b8a8cfc5befe1868f03e3b6bf8925b9b667cf67d4aeafc6d038398f2f11c3813e7b40ad79affdd29a9c491b3585bfa125d5 + version: 6.4.0 + resolution: "ansi-regex@npm:6.4.0" + checksum: 10/8d0305a47218c994a15348d556d358c9a0b3a7abbd51be1c502447813839fb2b0f12700fca9e842acdd79e5740bb2a2d291880b4d770c1634068732a2a3091e5 languageName: node linkType: hard @@ -3420,18 +3422,18 @@ __metadata: linkType: hard "array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9": - version: 3.1.9 - resolution: "array-includes@npm:3.1.9" + version: 3.2.0 + resolution: "array-includes@npm:3.2.0" dependencies: - call-bind: "npm:^1.0.8" + call-bind: "npm:^1.0.9" call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.24.0" - es-object-atoms: "npm:^1.1.1" - get-intrinsic: "npm:^1.3.0" + es-abstract: "npm:^1.24.2" + es-object-atoms: "npm:^1.1.2" + es-shim-unscopables: "npm:^1.1.0" is-string: "npm:^1.1.1" math-intrinsics: "npm:^1.1.0" - checksum: 10/8bfe9a58df74f326b4a76b04ee05c13d871759e888b4ee8f013145297cf5eb3c02cfa216067ebdaac5d74eb9763ac5cad77cdf2773b8ab475833701e032173aa + checksum: 10/c6b00774aa3f0100dbc5443722b69b0aa7ff5e31e28d2fc9ac69adc394c9fba4bc5cfea292bfbb20369a9657a8a67e0e9a128863cc0f9c426852e7cb11f6715e languageName: node linkType: hard @@ -3571,11 +3573,11 @@ __metadata: languageName: node linkType: hard -"babel-jest@npm:30.5.1": - version: 30.5.1 - resolution: "babel-jest@npm:30.5.1" +"babel-jest@npm:30.5.2": + version: 30.5.2 + resolution: "babel-jest@npm:30.5.2" dependencies: - "@jest/transform": "npm:30.5.1" + "@jest/transform": "npm:30.5.2" "@types/babel__core": "npm:^7.20.5" babel-plugin-istanbul: "npm:^8.0.0" babel-preset-jest: "npm:30.5.0" @@ -3584,7 +3586,7 @@ __metadata: slash: "npm:^3.0.0" peerDependencies: "@babel/core": ^7.11.0 || ^8.0.0-0 - checksum: 10/801caaf025b1f7f063a0f6b8cdf9bd9384d12f87e785887ff9b65df8987766d2cc118fca1157da157ad1567ad2569dd8838848f39c8a57341a56feebad5a37b8 + checksum: 10/52d08ac1a3130e94127e51892e3df8d61df647d0e6e66fdb24ac3c3ad5fb2466bf33d905f034a42056cf61e283874c1ef8c140e4e30728ad2da7ffc80b751473 languageName: node linkType: hard @@ -3727,12 +3729,12 @@ __metadata: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.11.20": - version: 2.11.21 - resolution: "baseline-browser-mapping@npm:2.11.21" +"baseline-browser-mapping@npm:^2.11.26": + version: 2.11.26 + resolution: "baseline-browser-mapping@npm:2.11.26" bin: baseline-browser-mapping: dist/cli.cjs - checksum: 10/b4d223df8dbb8deeaa3dae3b9b60ad48b24c57a5473827b5a6ee1c21917e7c37af2185224c5366f2932654ec5fb1a9fee4f95fe7213cc29f4347550f25e4234b + checksum: 10/e4ea8f668c420aa1d453be94e21a8fedd6824ecf624ffb49e1f5d3fc5b5baa3ce50205a02b552b020fc8528d781222050da2fc9dac15fb5cff7e076dfdc075c3 languageName: node linkType: hard @@ -3744,45 +3746,45 @@ __metadata: linkType: hard "brace-expansion@npm:^1.1.7": - version: 1.1.18 - resolution: "brace-expansion@npm:1.1.18" + version: 1.1.21 + resolution: "brace-expansion@npm:1.1.21" dependencies: balanced-match: "npm:^1.0.0" concat-map: "npm:0.0.1" - checksum: 10/b55a3c03239b8d2127c7cbb3408c9ad3a556a8c303bf3064df78bfb7c093160fc8f6e0a32e42a850a78eba12f29ccf6ef997690b9acf945d242c56c61c4aa977 + checksum: 10/6c0ed34107cb3bd3f44c01d90ec8c01baa3ed75b7755617e2e5797b5908bbf20b313edf575612b200a783b84c7d1826aec2f7a2f03de9905bbfae5dc3dea6a95 languageName: node linkType: hard "brace-expansion@npm:^2.0.2": - version: 2.1.4 - resolution: "brace-expansion@npm:2.1.4" + version: 2.1.7 + resolution: "brace-expansion@npm:2.1.7" dependencies: balanced-match: "npm:^1.0.0" - checksum: 10/11e18dc39706037331abedbb742af1da733267042f94c0f08487ef1a63cee068c1859f8d5f95523869725191133eb1a69753de457ed4b76b7c187d9ea0646737 + checksum: 10/4763be07b688136db4f98f71ab96c4a1a73b6785f69864c130c748f4429be961c6a9a4129c0a81aad4f9cfb08e31a3cf2809b2427509bf2990d1490b7b5dd2d0 languageName: node linkType: hard "brace-expansion@npm:^5.0.8": - version: 5.0.9 - resolution: "brace-expansion@npm:5.0.9" + version: 5.0.12 + resolution: "brace-expansion@npm:5.0.12" dependencies: balanced-match: "npm:^4.0.2" - checksum: 10/d8683d612919212c7cf298861acd1c15101e7e2ad186e7ae9747390e5b3fc9e9b55ce71107570d32985784d9d88fbbe438d725863aed7b0f3d08d5f080535eec + checksum: 10/65b550f1836e3b43f512f12afbc3be96616eee5c353e16d70181b7dffec8c08beabffb51d69a3bbdb27d675840725cbab025fda9d10716b5cda7dd864dc7bc4a languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.24.0, browserslist@npm:^4.28.1, browserslist@npm:^4.28.2, browserslist@npm:^4.28.4, browserslist@npm:^4.28.7": - version: 4.28.9 - resolution: "browserslist@npm:4.28.9" +"browserslist@npm:^4.0.0, browserslist@npm:^4.24.0, browserslist@npm:^4.28.1, browserslist@npm:^4.28.2, browserslist@npm:^4.28.7": + version: 4.29.3 + resolution: "browserslist@npm:4.29.3" dependencies: - baseline-browser-mapping: "npm:^2.11.20" - caniuse-lite: "npm:^1.0.30001810" - electron-to-chromium: "npm:^1.5.420" - node-releases: "npm:^2.0.54" - update-browserslist-db: "npm:^1.3.2" + baseline-browser-mapping: "npm:^2.11.26" + caniuse-lite: "npm:^1.0.30001813" + electron-to-chromium: "npm:^1.5.439" + node-releases: "npm:^2.0.57" + update-browserslist-db: "npm:^1.3.3" bin: browserslist: cli.js - checksum: 10/493e5e3c11d4bdd34ce8cb7ed5edc98bc5948457e6a3cc1268b253d0ff9a616033b10980515e0e6faeddae745fb9c330cd4510189d681ad86f9ce0b2da3c840e + checksum: 10/9649d4d73fff11f7c0b19948f08f9f287c184feb0965ac0b489cee7e1a9f430e747809db0d6014a30fdbcc42755e7a4dd047dfd7e28f62982072dcd0f8291eb0 languageName: node linkType: hard @@ -3890,10 +3892,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001810": - version: 1.0.30001810 - resolution: "caniuse-lite@npm:1.0.30001810" - checksum: 10/47d7db627c3f3d1a10e06eef9efbb84295c6ef9d959b585b64032cc293637ca7b9b6af7e5d5752972b3e690d9561f14b9561db03890fbc6e38dc1aaa54e51d97 +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001813": + version: 1.0.30001814 + resolution: "caniuse-lite@npm:1.0.30001814" + checksum: 10/387fa630eeb43c9a6ff5e669da4cc3010b611bfb43118ffa4d6ff89374b6245e95643bb4ca20058caf7d5ab3e46cf87f4a3f05346cc85ad23631618a5295754d languageName: node linkType: hard @@ -4549,10 +4551,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.420": - version: 1.5.425 - resolution: "electron-to-chromium@npm:1.5.425" - checksum: 10/c8bf467e0b639c5ffac58a046afb5d8088b5aae2513dd017096ee89f26fc7585c37f56ec6c6e71fdbc75fc31120c5736961c78a79c71f2edb5e14392319533b4 +"electron-to-chromium@npm:^1.5.439": + version: 1.5.443 + resolution: "electron-to-chromium@npm:1.5.443" + checksum: 10/9b29f0e215a8aeb44b76c1355636aaa16e4d2c4f7965eaaa15f5fac2efcff3429e255ba352c67d853e53a3d462eaca5f7174b7a434ba19803e08f4f4e83f2675 languageName: node linkType: hard @@ -4586,13 +4588,13 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.24.4": - version: 5.24.5 - resolution: "enhanced-resolve@npm:5.24.5" +"enhanced-resolve@npm:^5.25.0": + version: 5.26.0 + resolution: "enhanced-resolve@npm:5.26.0" dependencies: graceful-fs: "npm:^4.2.4" tapable: "npm:^2.3.3" - checksum: 10/cf3f6f62bd52c1d564d7967e474892d5b43690aedaf7a4e87c831fa6b4b065617baf08eb940907968c711965cbc6ccd773bcc67b8d87a71523c0512372be2a06 + checksum: 10/bd998505f0afd714a3c06ec24155fdc23101f5ace7887f36197857e388e7b2fb9616915c5c5eab6c240f12d9ff2fb09b53ab386593ef5d8664dcb07ff8447513 languageName: node linkType: hard @@ -4647,7 +4649,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0, es-abstract@npm:^1.24.2": +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.2": version: 1.24.2 resolution: "es-abstract@npm:1.24.2" dependencies: @@ -4988,13 +4990,13 @@ __metadata: linkType: hard "eslint-plugin-playwright@npm:^2.1.0": - version: 2.11.0 - resolution: "eslint-plugin-playwright@npm:2.11.0" + version: 2.12.0 + resolution: "eslint-plugin-playwright@npm:2.12.0" dependencies: globals: "npm:^17.3.0" peerDependencies: eslint: ">=8.40.0" - checksum: 10/2ada36903b2f5c03056b9f15a906e38adcea80a8500520af4a81e1d6033c981763c53322b5e6e48dd779332eab6a8651a6eadbbe539c7a200a4786ab0ed3e36f + checksum: 10/9a77f597d081070f56ba3d0115ac6cd8dc3fc5107f5bc9638b7b3007cc90dcf315785b4b61f1070ac8df2063a72532eed1820e47a8d88b7d11699d4327b0ff24 languageName: node linkType: hard @@ -5112,8 +5114,8 @@ __metadata: linkType: hard "eslint@npm:^10.8.0": - version: 10.10.0 - resolution: "eslint@npm:10.10.0" + version: 10.11.0 + resolution: "eslint@npm:10.11.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" "@eslint-community/regexpp": "npm:^4.12.2" @@ -5152,7 +5154,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/02fe7e79bf9f81d939ef3ad0c3151c0588f3441eef00bdc468797f2a9f8b047bea97c57a793fc97cf1e4886400b50f049b8e66ec1371820e9df33b54a8bc90d8 + checksum: 10/b4de8711cae009d6978e3760d400d5b2cba4117625764d9b1b77969c61f4db7dfa7bfb2754c0a22b3d9f35960a3e4f03bd579104d2c25894490bf3cf7b448cac languageName: node linkType: hard @@ -5248,17 +5250,17 @@ __metadata: languageName: node linkType: hard -"expect@npm:30.5.1": - version: 30.5.1 - resolution: "expect@npm:30.5.1" +"expect@npm:30.5.2": + version: 30.5.2 + resolution: "expect@npm:30.5.2" dependencies: - "@jest/expect-utils": "npm:30.5.1" + "@jest/expect-utils": "npm:30.5.2" "@jest/get-type": "npm:30.5.0" - jest-matcher-utils: "npm:30.5.1" + jest-matcher-utils: "npm:30.5.2" jest-message-util: "npm:30.5.1" - jest-mock: "npm:30.5.1" + jest-mock: "npm:30.5.2" jest-util: "npm:30.5.1" - checksum: 10/96188067d97c5130d534b8e360d449bce4a85b37c89132cf897fc6de7be2b3f378328e46d7ba925f33b43079bc902f1bbe7492b93af656fd8831f5ddd3ce48f0 + checksum: 10/92599f21f468172e4be9d6fadc6d373c5d838d1b5fdcc6fc72c9d3a83dafb6c4356bc4142b46622182b9450ab4f2978f19f701f2063ceffc9e02982386d296bf languageName: node linkType: hard @@ -5297,10 +5299,10 @@ __metadata: languageName: node linkType: hard -"fast-uri@npm:^3.1.7": - version: 3.1.7 - resolution: "fast-uri@npm:3.1.7" - checksum: 10/6d62ea818841e1b460f60482fd31582faa23ea9d9b33f856412b24c42da11fd72bd003be6696390ec896dbe4e9e78d3bb8d93bf70a511c7f69e5e807ac9b284f +"fast-uri@npm:^3.1.8": + version: 3.1.8 + resolution: "fast-uri@npm:3.1.8" + checksum: 10/d3dfa29fd2e5fa2371833699f3f78c975b79f65e52e0ce37604db10dffb85b5b448b5075e93453272cb95fb1c6a759495164e60649ffd69e59ec43f20d44f791 languageName: node linkType: hard @@ -5571,9 +5573,9 @@ __metadata: linkType: hard "globals@npm:^17.3.0, globals@npm:^17.8.0": - version: 17.12.0 - resolution: "globals@npm:17.12.0" - checksum: 10/833291560517dbb510c6d12359ba87eb8b8f282e9775b0474c6e19e0bea043b61f6b52732d4b1bb275888d1813564e40653e0d5faccef3083dc603cfbf0a3271 + version: 17.13.0 + resolution: "globals@npm:17.13.0" + checksum: 10/f723714cfb07bcf3163d44661d63b2a6076d93fd2f3724622b22084c62bcbbdb3ea3c601ec39635f1430d5cdf60bbf6fd7e458f12b5381ed422ed355c699537b languageName: node linkType: hard @@ -5658,7 +5660,7 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.2, hasown@npm:^2.0.3, hasown@npm:^2.0.4": +"hasown@npm:^2.0.2, hasown@npm:^2.0.4": version: 2.0.4 resolution: "hasown@npm:2.0.4" dependencies: @@ -5805,9 +5807,9 @@ __metadata: linkType: hard "ignore@npm:^7.0.5": - version: 7.0.9 - resolution: "ignore@npm:7.0.9" - checksum: 10/0364b6e5895a2e3b187654048de07c3369b4d106f51f8c0294c823f717aaf92d4f42d138e52d6d498bfc71f123eddb9d1fdb01977fd1cf1f29cc4f5ceaca2d77 + version: 7.0.11 + resolution: "ignore@npm:7.0.11" + checksum: 10/ccce62b7e13e9e404f5da774a5c8e696a8f173348bd30604d172baaa44bcde1b73bdcdda184a98471c3ba5a7c176604998b5c15d9e8124c7eda43f9f200a1083 languageName: node linkType: hard @@ -5925,11 +5927,11 @@ __metadata: linkType: hard "is-core-module@npm:^2.16.1, is-core-module@npm:^2.16.2": - version: 2.16.2 - resolution: "is-core-module@npm:2.16.2" + version: 2.17.0 + resolution: "is-core-module@npm:2.17.0" dependencies: - hasown: "npm:^2.0.3" - checksum: 10/6ee7535d82bbe457685799c5f145daf4b7c6be3afbd8e90788429d557f663d6dee72a8e4b9a45d0d756c243fcb5028095999243df090e5f04c02b153786bc8c6 + hasown: "npm:^2.0.4" + checksum: 10/0020f71b7eb525ccd684012cd1cfba235e7596e0addbc993732f7d8faff7ccd3a399e16dac7eb317b63459ea23f97efd661d6bf32584f6bf1d0a607fd8e21a6b languageName: node linkType: hard @@ -6271,45 +6273,45 @@ __metadata: languageName: node linkType: hard -"jest-circus@npm:30.5.1": - version: 30.5.1 - resolution: "jest-circus@npm:30.5.1" +"jest-circus@npm:30.5.2": + version: 30.5.2 + resolution: "jest-circus@npm:30.5.2" dependencies: - "@jest/environment": "npm:30.5.1" - "@jest/expect": "npm:30.5.1" - "@jest/test-result": "npm:30.5.1" + "@jest/environment": "npm:30.5.2" + "@jest/expect": "npm:30.5.2" + "@jest/test-result": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" chalk: "npm:^4.1.2" co: "npm:^4.6.0" dedent: "npm:^1.6.0" is-generator-fn: "npm:^2.1.0" - jest-each: "npm:30.5.1" - jest-matcher-utils: "npm:30.5.1" + jest-each: "npm:30.5.2" + jest-matcher-utils: "npm:30.5.2" jest-message-util: "npm:30.5.1" - jest-runtime: "npm:30.5.1" - jest-snapshot: "npm:30.5.1" + jest-runtime: "npm:30.5.2" + jest-snapshot: "npm:30.5.2" jest-util: "npm:30.5.1" p-limit: "npm:^3.1.0" pretty-format: "npm:30.5.1" pure-rand: "npm:^7.0.0" slash: "npm:^3.0.0" stack-utils: "npm:^2.0.6" - checksum: 10/509c28008e07deeaed6b33c0dcb767f29be9ad4c939d43e5827fdaff93838359f16bda17814c2b8f5323e430a6cb1cc25aa084e3f86b205564a095fdcd132be0 + checksum: 10/c203c9d5a18a70fb58ec402b538a8a47f01a73c050abdbca14fed15b2a3b3fe1d7efe166ec2450fecf032a4e016b371153489a0d8575ac0943e7ffbccf328462 languageName: node linkType: hard -"jest-cli@npm:30.5.1": - version: 30.5.1 - resolution: "jest-cli@npm:30.5.1" +"jest-cli@npm:30.5.2": + version: 30.5.2 + resolution: "jest-cli@npm:30.5.2" dependencies: - "@jest/core": "npm:30.5.1" - "@jest/test-result": "npm:30.5.1" + "@jest/core": "npm:30.5.2" + "@jest/test-result": "npm:30.5.2" "@jest/types": "npm:30.5.1" chalk: "npm:^4.1.2" exit-x: "npm:^0.2.2" import-local: "npm:^3.2.0" - jest-config: "npm:30.5.1" + jest-config: "npm:30.5.2" jest-util: "npm:30.5.1" jest-validate: "npm:30.5.1" yargs: "npm:^17.7.2" @@ -6320,31 +6322,31 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 10/54463bfa9afeb5f750f220dbc6518dbe647eed483843cd57c3811ae7ec7cc1fc065982277328a2a0f9858222c95e792ab8062bc7648076dd76b6b3591d761268 + checksum: 10/0ad4941a840534caf0be7c13a1900270463b91c69de0ad25fa62e0f5a6b00dc76481bbcf7432d9219d37eff84a706f7dd86919f00d0e3cc3cfef7a086ba14972 languageName: node linkType: hard -"jest-config@npm:30.5.1": - version: 30.5.1 - resolution: "jest-config@npm:30.5.1" +"jest-config@npm:30.5.2": + version: 30.5.2 + resolution: "jest-config@npm:30.5.2" dependencies: "@babel/core": "npm:^7.27.4" "@jest/get-type": "npm:30.5.0" "@jest/pattern": "npm:30.5.0" - "@jest/test-sequencer": "npm:30.5.1" + "@jest/test-sequencer": "npm:30.5.2" "@jest/types": "npm:30.5.1" - babel-jest: "npm:30.5.1" + babel-jest: "npm:30.5.2" chalk: "npm:^4.1.2" ci-info: "npm:^4.2.0" deepmerge: "npm:^4.3.1" glob: "npm:^13.0.6" graceful-fs: "npm:^4.2.11" - jest-circus: "npm:30.5.1" + jest-circus: "npm:30.5.2" jest-docblock: "npm:30.5.0" - jest-environment-node: "npm:30.5.1" + jest-environment-node: "npm:30.5.2" jest-regex-util: "npm:30.5.0" jest-resolve: "npm:30.5.1" - jest-runner: "npm:30.5.1" + jest-runner: "npm:30.5.2" jest-util: "npm:30.5.1" jest-validate: "npm:30.5.1" parse-json: "npm:^5.2.0" @@ -6362,19 +6364,19 @@ __metadata: optional: true ts-node: optional: true - checksum: 10/eec74659bf10923a745ace08efa97dd586ae0e665800676354d16de7f30d7611a45325486c2536cf4a8bfa6f377c383245471d375dd197384823407f7f780b39 + checksum: 10/3d8f20e6c8d42be7e5ba6df720f34039c8c0d0687b5a96f01c59a618075d295f1da0f36a22c49cf080db1b26c12c8d4618dcb9a51fe7df03692fc6232bc1a22c languageName: node linkType: hard -"jest-diff@npm:30.5.1": - version: 30.5.1 - resolution: "jest-diff@npm:30.5.1" +"jest-diff@npm:30.5.2": + version: 30.5.2 + resolution: "jest-diff@npm:30.5.2" dependencies: "@jest/diff-sequences": "npm:30.5.0" "@jest/get-type": "npm:30.5.0" chalk: "npm:^4.1.2" pretty-format: "npm:30.5.1" - checksum: 10/0779711058c09db86d1e3aeb9b571df9ede87e951a4f3f3c7082dbf6c5332749ee1d6464b179052418164084741c799ee6f4e08807c370baab40e2681ac500f3 + checksum: 10/42c2f228f605438e2f6c128290365fce29914508fbb96a8bfda41000a2437d8de9718b77ad97d40e65d0e36ea05a4b06227137f669869e11f2fa08c92df5a4be languageName: node linkType: hard @@ -6387,25 +6389,26 @@ __metadata: languageName: node linkType: hard -"jest-each@npm:30.5.1": - version: 30.5.1 - resolution: "jest-each@npm:30.5.1" +"jest-each@npm:30.5.2": + version: 30.5.2 + resolution: "jest-each@npm:30.5.2" dependencies: "@jest/get-type": "npm:30.5.0" "@jest/types": "npm:30.5.1" chalk: "npm:^4.1.2" + jest-regex-util: "npm:30.5.0" jest-util: "npm:30.5.1" pretty-format: "npm:30.5.1" - checksum: 10/41d9278e53001ab92e7a82fc26836e315e2d41420005c662cd92aff409ab0774ecc6cb359e2efa34fa58f1d9c6353a939d9efa272f2b8240aa81414cd978afee + checksum: 10/c5f1ee8707950f295709101757a9fd5ee21ac0ff5b827b6e04ce4a39a956a66e4d358487478050266f56ffe9697673fb2b5ae6665c1732435d44ea1f962eec60 languageName: node linkType: hard "jest-environment-jsdom@npm:^30.4.1": - version: 30.5.1 - resolution: "jest-environment-jsdom@npm:30.5.1" + version: 30.5.2 + resolution: "jest-environment-jsdom@npm:30.5.2" dependencies: - "@jest/environment": "npm:30.5.1" - "@jest/environment-jsdom-abstract": "npm:30.5.1" + "@jest/environment": "npm:30.5.2" + "@jest/environment-jsdom-abstract": "npm:30.5.2" "@types/jsdom": "npm:^21.1.7" jsdom: "npm:^26.1.0" peerDependencies: @@ -6413,22 +6416,22 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10/4ef724b7e951aca287ddde80a752985852240fb9c5ed2f0d56a54e2afd4fa2a6f99c8321456f6b3ee5e102bac6f18c874109c98ae2badcb0e65203c0bbf8bc21 + checksum: 10/8f316ec57f973de7f7ec7151d33c0038a4c6ed3d02e0cac910d69dacb805cd0f21133fea4b1f575c8d5c8a33d71a2c1a320e2d710df16dd0901284ecff9fecd6 languageName: node linkType: hard -"jest-environment-node@npm:30.5.1": - version: 30.5.1 - resolution: "jest-environment-node@npm:30.5.1" +"jest-environment-node@npm:30.5.2": + version: 30.5.2 + resolution: "jest-environment-node@npm:30.5.2" dependencies: - "@jest/environment": "npm:30.5.1" - "@jest/fake-timers": "npm:30.5.1" + "@jest/environment": "npm:30.5.2" + "@jest/fake-timers": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" - jest-mock: "npm:30.5.1" + jest-mock: "npm:30.5.2" jest-util: "npm:30.5.1" jest-validate: "npm:30.5.1" - checksum: 10/94bbb62d22d790cdaa46fa88742fcbeb0f2a70edd2ca03f9847b940ff303d247afc40a7e74346573f55f83a629df88a62812e9d23e19b5081093708d7434b52d + checksum: 10/6ddf88441fd085c3ea29b0b8dec4fc72974ac403fe114986b47d7831517aae0bded95e92b128c0d4a34bdacfa0101cb63a0c8d15c251937308b8266f1c76a9ba languageName: node linkType: hard @@ -6471,15 +6474,15 @@ __metadata: languageName: node linkType: hard -"jest-matcher-utils@npm:30.5.1": - version: 30.5.1 - resolution: "jest-matcher-utils@npm:30.5.1" +"jest-matcher-utils@npm:30.5.2": + version: 30.5.2 + resolution: "jest-matcher-utils@npm:30.5.2" dependencies: "@jest/get-type": "npm:30.5.0" chalk: "npm:^4.1.2" - jest-diff: "npm:30.5.1" + jest-diff: "npm:30.5.2" pretty-format: "npm:30.5.1" - checksum: 10/2840450962142eb45d2d8528049368d23dc67c333f93011eb98f0311f7368bd9d297a75965d74052336e4685a266fb71e8fd9fac9513004ac0dbe5fff919741b + checksum: 10/c3a772f30d3cddb8bf32bad82cc87104dcfc853694fcd212fe2e0fadda3bedebb8a99cb8688f780e1118c12275395e90b27fd53f83c74560cc8eb2974e6a2463 languageName: node linkType: hard @@ -6501,15 +6504,15 @@ __metadata: languageName: node linkType: hard -"jest-mock@npm:30.5.1": - version: 30.5.1 - resolution: "jest-mock@npm:30.5.1" +"jest-mock@npm:30.5.2": + version: 30.5.2 + resolution: "jest-mock@npm:30.5.2" dependencies: - "@jest/expect-utils": "npm:30.5.1" + "@jest/expect-utils": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" jest-util: "npm:30.5.1" - checksum: 10/41af43d7732d64505285b3f36853c5bb8d489c25e17f4823e6470d6d4083ea481e04f769fa2b5a7e7157c0fe9c229a66462fa79c84733a03cedaebe7a5f9d4da + checksum: 10/4a605ae054b317afef942a886285c65afe30fb2de7f883d0fd889bbbe01da2cdd3cebbbdd5b6fba57a3309c80d32105d1418bb4893e6f3ccf4529b61d8bc62d2 languageName: node linkType: hard @@ -6520,13 +6523,13 @@ __metadata: languageName: node linkType: hard -"jest-resolve-dependencies@npm:30.5.1": - version: 30.5.1 - resolution: "jest-resolve-dependencies@npm:30.5.1" +"jest-resolve-dependencies@npm:30.5.2": + version: 30.5.2 + resolution: "jest-resolve-dependencies@npm:30.5.2" dependencies: jest-regex-util: "npm:30.5.0" - jest-snapshot: "npm:30.5.1" - checksum: 10/a41deeed8276d922cbd91deb0a9aad9d24eba6084feef24660183527707c602b90123001e9ef0eae1e241851b45c6a233f7418a9a71bacb606731603e6d58c3d + jest-snapshot: "npm:30.5.2" + checksum: 10/cc6fd7cc877bbcc38c4b3fa7ca0eaa07cc6d9356096cd1e9c23a5f2ec1b9b3ad6cdca9833c5f9316162d862fc30140d9ff9e8ffe594f76685bae1d1f105347f1 languageName: node linkType: hard @@ -6545,15 +6548,15 @@ __metadata: languageName: node linkType: hard -"jest-runner@npm:30.5.1": - version: 30.5.1 - resolution: "jest-runner@npm:30.5.1" +"jest-runner@npm:30.5.2": + version: 30.5.2 + resolution: "jest-runner@npm:30.5.2" dependencies: - "@jest/console": "npm:30.5.1" - "@jest/environment": "npm:30.5.1" - "@jest/source-map": "npm:30.5.0" - "@jest/test-result": "npm:30.5.1" - "@jest/transform": "npm:30.5.1" + "@jest/console": "npm:30.5.2" + "@jest/environment": "npm:30.5.2" + "@jest/source-map": "npm:30.5.2" + "@jest/test-result": "npm:30.5.2" + "@jest/transform": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" chalk: "npm:^4.1.2" @@ -6561,30 +6564,30 @@ __metadata: exit-x: "npm:^0.2.2" graceful-fs: "npm:^4.2.11" jest-docblock: "npm:30.5.0" - jest-environment-node: "npm:30.5.1" + jest-environment-node: "npm:30.5.2" jest-haste-map: "npm:30.5.1" jest-leak-detector: "npm:30.5.1" jest-message-util: "npm:30.5.1" jest-resolve: "npm:30.5.1" - jest-runtime: "npm:30.5.1" + jest-runtime: "npm:30.5.2" jest-util: "npm:30.5.1" - jest-watcher: "npm:30.5.1" + jest-watcher: "npm:30.5.2" jest-worker: "npm:30.5.1" p-limit: "npm:^3.1.0" - checksum: 10/12ba5d8f3c14d1e96835bce0d12a5c391dc6ddf6d00422d4721d0c87bcb54cf237da9e9e0ee577e4845123a9624c5976bbc5f5268ec6e4fcd8cb67ff07dc4036 + checksum: 10/936c50973724d26daa8eb4152e7a5e853dc60a6ff22f14239b9f53349787a81e9e075232bcecaa8062ae27f0de603dce79894da8095d178538c28bf51da3ed89 languageName: node linkType: hard -"jest-runtime@npm:30.5.1": - version: 30.5.1 - resolution: "jest-runtime@npm:30.5.1" - dependencies: - "@jest/environment": "npm:30.5.1" - "@jest/fake-timers": "npm:30.5.1" - "@jest/globals": "npm:30.5.1" - "@jest/source-map": "npm:30.5.0" - "@jest/test-result": "npm:30.5.1" - "@jest/transform": "npm:30.5.1" +"jest-runtime@npm:30.5.2": + version: 30.5.2 + resolution: "jest-runtime@npm:30.5.2" + dependencies: + "@jest/environment": "npm:30.5.2" + "@jest/fake-timers": "npm:30.5.2" + "@jest/globals": "npm:30.5.2" + "@jest/source-map": "npm:30.5.2" + "@jest/test-result": "npm:30.5.2" + "@jest/transform": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" chalk: "npm:^4.1.2" @@ -6595,43 +6598,43 @@ __metadata: graceful-fs: "npm:^4.2.11" jest-haste-map: "npm:30.5.1" jest-message-util: "npm:30.5.1" - jest-mock: "npm:30.5.1" + jest-mock: "npm:30.5.2" jest-regex-util: "npm:30.5.0" jest-resolve: "npm:30.5.1" - jest-snapshot: "npm:30.5.1" + jest-snapshot: "npm:30.5.2" jest-util: "npm:30.5.1" slash: "npm:^3.0.0" strip-bom: "npm:^4.0.0" - checksum: 10/40ffae68059b87e83549b1b001e455f4f51b8a4a3daa066c7a2ee1bd4d84c152a595d964c3d92fded5901dc9f23bae28eca8b129a19f740f77af23f18d1e8ba3 + checksum: 10/d44355c4157bddca99e924a98f48fdc158c07ff1539b2231f2f593fae2a22ce31ae22845b9f5cd2b15a2f5f39870ed7313a99b096420f02b15984de833566287 languageName: node linkType: hard -"jest-snapshot@npm:30.5.1": - version: 30.5.1 - resolution: "jest-snapshot@npm:30.5.1" +"jest-snapshot@npm:30.5.2": + version: 30.5.2 + resolution: "jest-snapshot@npm:30.5.2" dependencies: "@babel/core": "npm:^7.27.4" "@babel/generator": "npm:^7.27.5" "@babel/plugin-syntax-jsx": "npm:^7.27.1" "@babel/plugin-syntax-typescript": "npm:^7.27.1" "@babel/types": "npm:^7.27.3" - "@jest/expect-utils": "npm:30.5.1" + "@jest/expect-utils": "npm:30.5.2" "@jest/get-type": "npm:30.5.0" "@jest/snapshot-utils": "npm:30.5.1" - "@jest/transform": "npm:30.5.1" + "@jest/transform": "npm:30.5.2" "@jest/types": "npm:30.5.1" babel-preset-current-node-syntax: "npm:^1.2.0" chalk: "npm:^4.1.2" - expect: "npm:30.5.1" + expect: "npm:30.5.2" graceful-fs: "npm:^4.2.11" - jest-diff: "npm:30.5.1" - jest-matcher-utils: "npm:30.5.1" + jest-diff: "npm:30.5.2" + jest-matcher-utils: "npm:30.5.2" jest-message-util: "npm:30.5.1" jest-util: "npm:30.5.1" pretty-format: "npm:30.5.1" semver: "npm:^7.7.2" synckit: "npm:^0.11.8" - checksum: 10/8044e806e78f2033eb93e13ba7bec2cc391f3807a6c3b022a543cab28c92ca1780c76bc366dbe3e63995917cae99e475e6f4af1415dc58fb5a6ffdec71a42c0a + checksum: 10/64be725cd1171c940831c30cef6f349a9b5e331d064ee6454607e59da98d2a4141cb66eb5d208346e775f8ca3c199114f8fb1853218b9112a2b37cd7d572a648 languageName: node linkType: hard @@ -6663,11 +6666,11 @@ __metadata: languageName: node linkType: hard -"jest-watcher@npm:30.5.1": - version: 30.5.1 - resolution: "jest-watcher@npm:30.5.1" +"jest-watcher@npm:30.5.2": + version: 30.5.2 + resolution: "jest-watcher@npm:30.5.2" dependencies: - "@jest/test-result": "npm:30.5.1" + "@jest/test-result": "npm:30.5.2" "@jest/types": "npm:30.5.1" "@types/node": "npm:*" ansi-escapes: "npm:^4.3.2" @@ -6675,7 +6678,7 @@ __metadata: emittery: "npm:^0.13.1" jest-util: "npm:30.5.1" string-length: "npm:^4.0.2" - checksum: 10/890ba0b3b9b2d61871cdf55b04d573c73c43c0f158514ef47bd6277cd8476f1035803b470331c01525d00cbee133b807ab281c55d9048f9db632c3575a9f884a + checksum: 10/aec43f7cf8db5e640a2a3f63676da77cd55d8b6bbc7cc0b563a03fc6c263bd1ec88eba02f0cdd639dfe4114f516c7c55911984e20570325eb77cc22cdb720871 languageName: node linkType: hard @@ -6704,13 +6707,13 @@ __metadata: linkType: hard "jest@npm:^30.4.2": - version: 30.5.1 - resolution: "jest@npm:30.5.1" + version: 30.5.2 + resolution: "jest@npm:30.5.2" dependencies: - "@jest/core": "npm:30.5.1" + "@jest/core": "npm:30.5.2" "@jest/types": "npm:30.5.1" import-local: "npm:^3.2.0" - jest-cli: "npm:30.5.1" + jest-cli: "npm:30.5.2" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -6718,7 +6721,7 @@ __metadata: optional: true bin: jest: bin/jest.js - checksum: 10/3b300b3ade7ccff731c42d53482bad784f053f99feacc4e1da1cc884d0778652d900cd870f03eff1ff6856bbdb0e087ef1316afbc1c79868b7023cb5ed6f731a + checksum: 10/687b2f290e25093c061878a1ca861ea8b6552dd3542c01b04764f84294bfd8697f716abe106a0f8ea3a6fb85c567f569e567e1cd1ca4d0a0f1e54b3291ce67c2 languageName: node linkType: hard @@ -6986,9 +6989,9 @@ __metadata: linkType: hard "lru-cache@npm:^11.0.0": - version: 11.5.2 - resolution: "lru-cache@npm:11.5.2" - checksum: 10/122789c66605ddf29df58ff279e9e2c9499499d0b80b895ec524496f14bcde045d1582e3e38008f3457226d98067556719573b352119d6be8bdb1f8849f85681 + version: 11.5.3 + resolution: "lru-cache@npm:11.5.3" + checksum: 10/824394f43827de37e961e9e344d4f0d2afb0347106ddafae620d0035c36337bc64e64cddbc17f6c630a73c9db34cf3f81211e3d43699a32e9f3641f391415b82 languageName: node linkType: hard @@ -7106,8 +7109,8 @@ __metadata: linkType: hard "minimizer-webpack-plugin@npm:^5.7.0": - version: 5.10.0 - resolution: "minimizer-webpack-plugin@npm:5.10.0" + version: 5.12.0 + resolution: "minimizer-webpack-plugin@npm:5.12.0" dependencies: "@jridgewell/trace-mapping": "npm:^0.3.31" jest-worker: "npm:^27.4.5" @@ -7148,7 +7151,7 @@ __metadata: optional: true uglify-js: optional: true - checksum: 10/11793b2d5164ca6e85611b7ec2117bb76da1be52dbe9c9978df6c61686bb932abdf9bb6b008c7c797bd81e0512ea20d950f60fb56e37165e7470b01f66ee0458 + checksum: 10/d12db1ee1cb6fc5910e7ee12efe538082308262eb706ce31ed493bebc8649b03a46ab94244c94949f831a9c0b67e3a0ddd8119632c2bdda466f5ee8ebe0c409d languageName: node linkType: hard @@ -7207,13 +7210,6 @@ __metadata: languageName: node linkType: hard -"neo-async@npm:^2.6.2": - version: 2.6.2 - resolution: "neo-async@npm:2.6.2" - checksum: 10/1a7948fea86f2b33ec766bc899c88796a51ba76a4afc9026764aedc6e7cde692a09067031e4a1bf6db4f978ccd99e7f5b6c03fe47ad9865c3d4f99050d67e002 - languageName: node - linkType: hard - "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -7286,10 +7282,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.54": - version: 2.0.55 - resolution: "node-releases@npm:2.0.55" - checksum: 10/1ca12c725eeaf9d4645a4ed159c19ae5ce8303a0063443c95fb5d97edc21c0d3c6d55ae7d96ae21724ca73800ea0149ea60af73f3ed0ddb10f0b05da84d36efc +"node-releases@npm:^2.0.57": + version: 2.0.57 + resolution: "node-releases@npm:2.0.57" + checksum: 10/aaffaea5ab8885f88df52ce5f96d59737f9d3fe122ae65cd94caf8876628ff2260afabcfaec04d0ec64f4f52994d99f888ea8a2373cf2203ef8c2b3b14314ce6 languageName: node linkType: hard @@ -7330,9 +7326,9 @@ __metadata: linkType: hard "nwsapi@npm:^2.2.16": - version: 2.2.27 - resolution: "nwsapi@npm:2.2.27" - checksum: 10/d4b9e77402ee91b9ca2ef280832c5b39f0835deb83ff186268d9b039a610f00405754a8f291347cd871ea3b09f46b5def91c4200c241979acb8383557b3b4e19 + version: 2.2.28 + resolution: "nwsapi@npm:2.2.28" + checksum: 10/2395c1d894d2a11c7299c92f62e51849b6a02256d48a9a40c64a40e32ca06b188145cd984256a3134a6930794623abf9d7df9ab9f31dd63f471d1f999efb4830 languageName: node linkType: hard @@ -8069,11 +8065,11 @@ __metadata: linkType: hard "prettier@npm:^3.9.6": - version: 3.9.6 - resolution: "prettier@npm:3.9.6" + version: 3.9.9 + resolution: "prettier@npm:3.9.9" bin: prettier: bin/prettier.cjs - checksum: 10/1dd1a1e0e40ec3b91cda9d294c4a95022aef61880ca3f441aad99b1252279f58a766c4cece81132c01550dcff1fd445efbd8812ea4a2374313e7fc6c8136f11f + checksum: 10/3d11a8095a7db50f0fbc8086ad96be676401154b63a2e9023e1274893457dccab19a9c211f4fb2cae7244e800bea2d5185d9bb6fc510f48a53017815e47d4e6d languageName: node linkType: hard @@ -8171,7 +8167,7 @@ __metadata: languageName: node linkType: hard -"reflect.getprototypeof@npm:^1.0.10, reflect.getprototypeof@npm:^1.0.9": +"reflect.getprototypeof@npm:^1.0.10": version: 1.0.10 resolution: "reflect.getprototypeof@npm:1.0.10" dependencies: @@ -8187,12 +8183,12 @@ __metadata: languageName: node linkType: hard -"regenerate-unicode-properties@npm:^10.2.2": - version: 10.2.2 - resolution: "regenerate-unicode-properties@npm:10.2.2" +"regenerate-unicode-properties@npm:^10.3.0": + version: 10.3.0 + resolution: "regenerate-unicode-properties@npm:10.3.0" dependencies: regenerate: "npm:^1.4.2" - checksum: 10/5041ee31185c4700de9dd76783fab9def51c412751190d523d621db5b8e35a6c2d91f1642c12247e7d94f84b8ae388d044baac1e88fc2ba0ac215ca8dc7bed38 + checksum: 10/15d9c4a2e640b3ff05d44ba67a4c07cae8dcdfa3c6c1c2ff979f6c68161aa36bb61608a642c5c9c678ff70593c9e0b7573f84aef1f0e9c33f08c567d3708695a languageName: node linkType: hard @@ -8218,16 +8214,16 @@ __metadata: linkType: hard "regexpu-core@npm:^6.3.1": - version: 6.4.0 - resolution: "regexpu-core@npm:6.4.0" + version: 6.5.2 + resolution: "regexpu-core@npm:6.5.2" dependencies: regenerate: "npm:^1.4.2" - regenerate-unicode-properties: "npm:^10.2.2" + regenerate-unicode-properties: "npm:^10.3.0" regjsgen: "npm:^0.8.0" regjsparser: "npm:^0.13.0" unicode-match-property-ecmascript: "npm:^2.0.0" unicode-match-property-value-ecmascript: "npm:^2.2.1" - checksum: 10/bf5f85a502a17f127a1f922270e2ecc1f0dd071ff76a3ec9afcd6b1c2bf7eae1486d1e3b1a6d621aee8960c8b15139e6b5058a84a68e518e1a92b52e9322faf9 + checksum: 10/ee8fd7672c20e44a618d9870f76a04a28df0919d5fd9353b7b079785af9bc45f1cd3e2409dab0137849f261a3a78a0c9e7de17b646d943ec7d32925fd3153c85 languageName: node linkType: hard @@ -8239,13 +8235,13 @@ __metadata: linkType: hard "regjsparser@npm:^0.13.0": - version: 0.13.2 - resolution: "regjsparser@npm:0.13.2" + version: 0.13.3 + resolution: "regjsparser@npm:0.13.3" dependencies: jsesc: "npm:~3.1.0" bin: regjsparser: bin/parser - checksum: 10/291aecbd47371cee347a96c47ccaae729ba50b7b2cb2a5de7e088e2ab835fe133569422f06ae28f5ff0830ac03f3196a35ba493f23ecda086d82e3e326f14074 + checksum: 10/ac401addf8e87c99a16ca6f047197bebd8fe31c2d8d18871b9055b1e42f3a9301724a268767e79cfb51eebdc38c13ab10cbf009cc2d7d32cd234fd2a979114f1 languageName: node linkType: hard @@ -8438,15 +8434,15 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3": - version: 4.3.3 - resolution: "schema-utils@npm:4.3.3" +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3, schema-utils@npm:^4.5.0": + version: 4.5.0 + resolution: "schema-utils@npm:4.5.0" dependencies: - "@types/json-schema": "npm:^7.0.9" - ajv: "npm:^8.9.0" - ajv-formats: "npm:^2.1.1" + "@types/json-schema": "npm:^7.0.15" + ajv: "npm:^8.20.0" + ajv-formats: "npm:^3.0.1" ajv-keywords: "npm:^5.1.0" - checksum: 10/dba77a46ad7ff0c906f7f09a1a61109e6cb56388f15a68070b93c47a691f516c6a3eb454f81a8cceb0a0e55b87f8b05770a02bfb1f4e0a3143b5887488b2f900 + checksum: 10/a5c1167f5bbcce3bd4f2031b58cbd77b8bad6d8ec0278bd9c68409aa6142758def38d78a2f05fa4ce157408ed9cea6457d05d88b6368c39b8b598f59e39b19b8 languageName: node linkType: hard @@ -8479,10 +8475,10 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^7.0.6": - version: 7.1.1 - resolution: "serialize-javascript@npm:7.1.1" - checksum: 10/fa20ff05b658803e8a1e4417df393e2b3afa33518682f5beecfc6a2082029fc9fd4e9aff1c16630af2fae4157dc701d15372e48d979f04a3c4f6f7a2c5bbeeae +"serialize-javascript@npm:^7.1.2": + version: 7.1.2 + resolution: "serialize-javascript@npm:7.1.2" + checksum: 10/e5550ef83b945196f563d33b3c80767e2f27f8c3856b4d19e515f5fd881273d80b9f7fced9247c8ed7cafff5c4de244cf93ed45fd6dc572c4aec303cd47f8dfc languageName: node linkType: hard @@ -8628,9 +8624,9 @@ __metadata: linkType: hard "source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1": - version: 1.2.1 - resolution: "source-map-js@npm:1.2.1" - checksum: 10/ff9d8c8bf096d534a5b7707e0382ef827b4dd360a577d3f34d2b9f48e12c9d230b5747974ee7c607f0df65113732711bb701fe9ece3c7edbd43cb2294d707df3 + version: 1.2.2 + resolution: "source-map-js@npm:1.2.2" + checksum: 10/4807c894bb6280c8b84886159b96080e3996f42c6e8015e647881edfd4f955f6024ceebb8bb40d33ce8b106e24b2e29c133575e1c60c01c8aae57a05fdf17761 languageName: node linkType: hard @@ -8669,9 +8665,9 @@ __metadata: linkType: hard "spdx-license-ids@npm:^3.0.0": - version: 3.0.23 - resolution: "spdx-license-ids@npm:3.0.23" - checksum: 10/fead6be44478e4dd73a0721ae569f4a04f358846d8d82e8d92efae64aca928592e380cf17e8b84c25c948f3a7d8a0b4fc781a1830f3911ca87d52733265176b5 + version: 3.0.24 + resolution: "spdx-license-ids@npm:3.0.24" + checksum: 10/f8aaa5e869c9b4ad1bb4b3c1f1ef234bb10fda65e53879d918f3126710af1bd2df89cc4fe6400da90b3281317d909fd035e54e1c2bd4d1b10fb7ac1d30a420e7 languageName: node linkType: hard @@ -9152,17 +9148,16 @@ __metadata: linkType: hard "typed-array-byte-offset@npm:^1.0.4": - version: 1.0.4 - resolution: "typed-array-byte-offset@npm:1.0.4" + version: 1.0.5 + resolution: "typed-array-byte-offset@npm:1.0.5" dependencies: available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - for-each: "npm:^0.3.3" + call-bind: "npm:^1.0.9" + for-each: "npm:^0.3.5" gopd: "npm:^1.2.0" - has-proto: "npm:^1.2.0" is-typed-array: "npm:^1.1.15" - reflect.getprototypeof: "npm:^1.0.9" - checksum: 10/c2869aa584cdae24ecfd282f20a0f556b13a49a9d5bca1713370bb3c89dff0ccbc5ceb45cb5b784c98f4579e5e3e2a07e438c3a5b8294583e2bd4abbd5104fb5 + reflect.getprototypeof: "npm:^1.0.10" + checksum: 10/c7054706f6957a406c9546db88787ecd389f3e468182b1e05dd07175ca263fbf672c6f773074d38d1832a0cab20cdb2138a45f18960c828f4c9d46cac14a5696 languageName: node linkType: hard @@ -9181,17 +9176,17 @@ __metadata: linkType: hard "typescript-eslint@npm:^8.57.1": - version: 8.70.0 - resolution: "typescript-eslint@npm:8.70.0" + version: 8.71.0 + resolution: "typescript-eslint@npm:8.71.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.70.0" - "@typescript-eslint/parser": "npm:8.70.0" - "@typescript-eslint/typescript-estree": "npm:8.70.0" - "@typescript-eslint/utils": "npm:8.70.0" + "@typescript-eslint/eslint-plugin": "npm:8.71.0" + "@typescript-eslint/parser": "npm:8.71.0" + "@typescript-eslint/typescript-estree": "npm:8.71.0" + "@typescript-eslint/utils": "npm:8.71.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: ">=4.8.4 <6.1.0" - checksum: 10/afdb2760ff425e806165d516e5d5baea4b446ce75c8e039a71d225047550a07b712f76b4b4a48982613f881e9694155e1beff1c5f0c4d034286048a41448911c + checksum: 10/0f08789961c697b4835d199784d3c12e011a403b4068194af551c0ff9ea8401232e09da7a8ffa6c995b38dc1320aab04effb7d5c57d334416852ea063f4060a1 languageName: node linkType: hard @@ -9235,9 +9230,9 @@ __metadata: linkType: hard "undici@npm:^7.29.0": - version: 7.29.1 - resolution: "undici@npm:7.29.1" - checksum: 10/8b9eaf83615e96f83826c9b6b129bd378c26f750f183e931df89bf07dfae24a39736b3648c16c25904e59e34f9e151e7a852c82ef0f1fb8e8a1297c52ff3170c + version: 7.30.0 + resolution: "undici@npm:7.30.0" + checksum: 10/eb9e4bb5f1b4c35cb934c0d04fe676b62c3be06fb3be4a256ff73699bd77abc3d41f262597a4debd41f334c95ca3ea6e763268c3768882c3fc88a5d33234b8f6 languageName: node linkType: hard @@ -9348,9 +9343,9 @@ __metadata: languageName: node linkType: hard -"update-browserslist-db@npm:^1.3.2": - version: 1.3.2 - resolution: "update-browserslist-db@npm:1.3.2" +"update-browserslist-db@npm:^1.3.3": + version: 1.3.3 + resolution: "update-browserslist-db@npm:1.3.3" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -9358,7 +9353,7 @@ __metadata: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/ca883e9d0fca1b5bfb9d5e7d9468c5ddd1d66ea827566f474875171381ad33b84aa555c138094fb2c6297c680e06482ad8b4c1ad931e8bfab9a4cf36ea9cf006 + checksum: 10/c60c18c53ba53915b377830db55049cd3ee31129633b4e386d2e48f9a1a15ade7503a80a57dd11d66cbac583a8ad3f2d61e938895b1c2155323fd4367530f5b2 languageName: node linkType: hard @@ -9520,32 +9515,30 @@ __metadata: linkType: hard "webpack-sources@npm:^3.5.1": - version: 3.5.1 - resolution: "webpack-sources@npm:3.5.1" - checksum: 10/50b9699a3ab9a698e117d8a6b816235576f17e60aeca0a44269041773f960c3beb2ad10d07bc5f2ef8eebde3db5275c6a23566dad475db8f840a34a7bfabd2c3 + version: 3.6.0 + resolution: "webpack-sources@npm:3.6.0" + checksum: 10/7f89bc6a7f08b4b19dd7098ed92eac397648bc36d4c555ad54abad621ff60643ea94d2652cb708d57a3ccf10cec0471aec8dca254c9e30721157082ff66b9964 languageName: node linkType: hard "webpack@npm:^5.109.2": - version: 5.110.3 - resolution: "webpack@npm:5.110.3" + version: 5.111.1 + resolution: "webpack@npm:5.111.1" dependencies: "@types/estree": "npm:^1.0.8" "@types/json-schema": "npm:^7.0.15" "@webassemblyjs/ast": "npm:^1.14.1" "@webassemblyjs/wasm-edit": "npm:^1.14.1" "@webassemblyjs/wasm-parser": "npm:^1.14.1" - acorn: "npm:^8.16.0" browserslist: "npm:^4.28.1" chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.24.4" + enhanced-resolve: "npm:^5.25.0" es-module-lexer: "npm:^2.1.0" events: "npm:^3.2.0" graceful-fs: "npm:^4.2.11" mime-db: "npm:^1.54.0" minimizer-webpack-plugin: "npm:^5.7.0" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^4.3.3" + schema-utils: "npm:^4.5.0" tapable: "npm:^2.3.0" watchpack: "npm:^2.5.2" webpack-sources: "npm:^3.5.1" @@ -9554,7 +9547,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10/be12a8b532febb0ab47d1b98f1f243708926be1b9fc39bceecdf8876af907a93defc9b36c9cece3adfccfc76c6960909b187f488e8c3497e8ae989c0701f0e37 + checksum: 10/1ce6844f0e2709083c8e24165dfa526d08a52ad2dfd7d39eed87edb170618ad5e23ac50deb646b019c41d576eed071e6bf407b7f9fce2010643f9fc9a705d284 languageName: node linkType: hard @@ -9641,8 +9634,8 @@ __metadata: linkType: hard "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": - version: 1.1.22 - resolution: "which-typed-array@npm:1.1.22" + version: 1.1.24 + resolution: "which-typed-array@npm:1.1.24" dependencies: available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.9" @@ -9651,7 +9644,7 @@ __metadata: get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-tostringtag: "npm:^1.0.2" - checksum: 10/59b0383347e2f3b0bc5be570c2dfae551b172a9c83e0a6b03c6e17401d6161dfa1d912c7657062fe9add254a0d3c25ef70593dbaec8fefa8714715ff69e0a3fc + checksum: 10/5b9abdc4ff046414312eac5c5b2fedab57b08979209882049fd2f4064166f8f97edd6831ff701cb6171313da19dbbd091ed46f30712e733e9b166f5f5388159e languageName: node linkType: hard @@ -9724,8 +9717,8 @@ __metadata: linkType: hard "ws@npm:^8.18.0": - version: 8.21.3 - resolution: "ws@npm:8.21.3" + version: 8.22.0 + resolution: "ws@npm:8.22.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -9734,7 +9727,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/8856922c26fd2d106931c16310d9a0bc2d6d37ed8771352bfa0d2a36df0ab5cb1f6528c6db8213f3333cf4ce93925e56f13604df0454cb0d4289fef922bebcdd + checksum: 10/ed771c2766bdb5724a66872f60ffc1a39f06e1a5bc8849f453c273e97c91a40ed5a224f06dc0e22f44b4a9e03938bf746fc5938d6887433cc0f4c416237d3b10 languageName: node linkType: hard @@ -9819,8 +9812,8 @@ __metadata: linkType: hard "zod@npm:^3.25.0 || ^4.0.0": - version: 4.5.4 - resolution: "zod@npm:4.5.4" - checksum: 10/b2fd4aaf358359650a1f27f303a462f0f49b4e5582f517ad8894ed566fdceb9da391b59babffe3c70574c8064174883735657eb28e12db5c7021455a631866c4 + version: 4.6.5 + resolution: "zod@npm:4.6.5" + checksum: 10/db004952e4dc6eab38b70f0cdef9251a65da84f5d08c5a0ef85e2590cfa9c477544ce734161ff5c05f89a15f0967137607a7feb16716195e04965f030db47213 languageName: node linkType: hard