From 7a7e6901e450a795d92c604432c930d93075dc64 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 19 Aug 2025 21:13:58 +0100 Subject: [PATCH 01/16] Start making better use of exceptions. --- src/Exception/HTTPFailedException.php | 8 ++ src/Exception/InvalidHashException.php | 8 ++ src/Exception/MissingHashException.php | 8 ++ src/Exception/MissingURLException.php | 8 ++ src/Exception/UnexpectedHTTPCodeException.php | 8 ++ src/bootstrap.php | 93 ++++++++++++------- src/event.php | 9 +- tests/integration/URLEventTest.php | 34 +++++++ 8 files changed, 136 insertions(+), 40 deletions(-) create mode 100644 src/Exception/HTTPFailedException.php create mode 100644 src/Exception/InvalidHashException.php create mode 100644 src/Exception/MissingHashException.php create mode 100644 src/Exception/MissingURLException.php create mode 100644 src/Exception/UnexpectedHTTPCodeException.php create mode 100644 tests/integration/URLEventTest.php diff --git a/src/Exception/HTTPFailedException.php b/src/Exception/HTTPFailedException.php new file mode 100644 index 00000000..38877d31 --- /dev/null +++ b/src/Exception/HTTPFailedException.php @@ -0,0 +1,8 @@ + $args The event args array. * @phpstan-param array{ @@ -2687,29 +2698,38 @@ function action_url_cron_event( array $args ): void { 'hash' => $hash, ) = $args; + if ( empty( $url ) ) { + throw new MissingURLException( + esc_html( + sprintf( + 'WP Crontrol: The URL is missing for a URL cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), + ) + ) + ); + } + if ( empty( $hash ) ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( - sprintf( - 'WP Crontrol: The stored hash is missing for a URL cron event; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), - ), - E_USER_WARNING + throw new MissingHashException( + esc_html( + sprintf( + 'WP Crontrol: The stored hash is missing for a URL cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), + ) + ) ); - return; } // Check the integrity of the URL. if ( ! check_integrity( $url, $hash ) ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( - sprintf( - 'WP Crontrol: The stored hash for a URL cron event is not valid; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), - ), - E_USER_WARNING + throw new InvalidHashException( + esc_html( + sprintf( + 'WP Crontrol: The stored hash for a URL cron event is not valid; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), + ) + ) ); - return; } $request_args = array( @@ -2723,7 +2743,7 @@ function action_url_cron_event( array $args ): void { $response = wp_safe_remote_request( $url, $request_args ); if ( is_wp_error( $response ) ) { - throw new Exception( + throw new HTTPFailedException( esc_html( sprintf( 'WP Crontrol: Failed to fetch URL %s: %s', @@ -2738,7 +2758,7 @@ function action_url_cron_event( array $args ): void { $message = wp_remote_retrieve_response_message( $response ); if ( $code < 200 || $code >= 300 ) { - throw new Exception( + throw new UnexpectedHTTPCodeException( esc_html( sprintf( 'WP Crontrol: Unexpected response code for URL %s: HTTP %s %s', @@ -2780,6 +2800,9 @@ function action_url_cron_event( array $args ): void { * * @link https://wp-crontrol.com/docs/php-cron-events/ * + * @throws MissingHashException + * @throws InvalidHashException + * * @param array|string $args The event args array, or a string containing the PHP code to evaluate. * @phpstan-param array{ * code?: string, @@ -2806,28 +2829,26 @@ function action_php_cron_event( $args ): void { } if ( empty( $hash ) ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( - sprintf( - 'WP Crontrol: The stored hash is missing for a PHP cron event; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), - ), - E_USER_WARNING + throw new MissingHashException( + esc_html( + sprintf( + 'WP Crontrol: The stored hash is missing for a PHP cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + ), + ) ); - return; } // Check the integrity of the PHP code. if ( ! check_integrity( $code, $hash ) ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - trigger_error( - sprintf( - 'WP Crontrol: The stored hash for a PHP cron event is not valid; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), - ), - E_USER_WARNING + throw new InvalidHashException( + esc_html( + sprintf( + 'WP Crontrol: The stored hash for a PHP cron event is not valid; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + ), + ) ); - return; } // Please see the function description above for information about the safety of this code. diff --git a/src/event.php b/src/event.php index f6406983..812ebe0c 100644 --- a/src/event.php +++ b/src/event.php @@ -8,6 +8,7 @@ use stdClass; use Crontrol\Schedule; use WP_Error; +use InvalidArgumentException; use const Crontrol\PAUSED_OPTION; @@ -171,7 +172,7 @@ function add( $next_run_local, $schedule, $hook, array $args ) { if ( 'crontrol_url_cron_job' === $hook && ! empty( $args[0]['url'] ) ) { try { validate_url( $args[0]['url'] ); - } catch ( \InvalidArgumentException $e ) { + } catch ( InvalidArgumentException $e ) { $args[0]['url_error_message'] = $e->getMessage(); $error = $e; } @@ -581,7 +582,7 @@ function get_core_cron_array() { * * @see https://github.com/WordPress/wordpress-develop/blob/197f0a71ad27d0688b6380c869aeaf92addd1451/src/wp-includes/class-wp-http.php#L283-L299 * - * @throws \InvalidArgumentException If the URL is not valid or contains an invalid protocol. + * @throws InvalidArgumentException If the URL is not valid or contains an invalid protocol. * * @param string $url The URL to validate. */ @@ -589,7 +590,7 @@ function validate_url( string $url ): void { $valid = wp_http_validate_url( $url ); if ( $valid === false ) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( esc_html( sprintf( /* translators: %s: The URL that failed validation. */ @@ -603,7 +604,7 @@ function validate_url( string $url ): void { $filtered = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) ); if ( $filtered === '' ) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( esc_html( sprintf( /* translators: %s: The URL that failed validation. */ diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php new file mode 100644 index 00000000..ae38dcec --- /dev/null +++ b/tests/integration/URLEventTest.php @@ -0,0 +1,34 @@ +expectException( MissingHashException::class ); + + do_action( + 'crontrol_url_cron_job', + [ + 'url' => 'http://example.com', + 'method' => 'GET', + ] + ); + } + + public function testInvalidHashTriggersException(): void { + $this->expectException( InvalidHashException::class ); + + do_action( + 'crontrol_url_cron_job', + [ + 'url' => 'http://example.com', + 'method' => 'GET', + 'hash' => 'invalidhash', + ] + ); + } + +} From 2e2592eefd608b70e30b48c1e06d56e1da4a1abe Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 19 Aug 2025 21:18:32 +0100 Subject: [PATCH 02/16] These arguments are all technically optional. --- src/bootstrap.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/bootstrap.php b/src/bootstrap.php index 7ce12747..df246cbd 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -2685,18 +2685,16 @@ function json_output( $input, $pretty = true ) { * * @param array $args The event args array. * @phpstan-param array{ - * url: string, - * name: string, - * method: string, - * hash: string, + * url?: string, + * name?: string, + * method?: string, + * hash?: string, * } $args */ function action_url_cron_event( array $args ): void { - list( - 'url' => $url, - 'method' => $method, - 'hash' => $hash, - ) = $args; + $url = $args['url'] ?? null; + $method = $args['method'] ?? 'GET'; + $hash = $args['hash'] ?? null; if ( empty( $url ) ) { throw new MissingURLException( From 1ad3f6b91dce91cf3ea481118fcae8b8f573f529 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 19 Aug 2025 21:35:27 +0100 Subject: [PATCH 03/16] More tests. --- tests/integration/PHPEventTest.php | 49 ++++++++++++++++++++++++++++++ tests/integration/URLEventTest.php | 26 ++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 tests/integration/PHPEventTest.php diff --git a/tests/integration/PHPEventTest.php b/tests/integration/PHPEventTest.php new file mode 100644 index 00000000..94faa09e --- /dev/null +++ b/tests/integration/PHPEventTest.php @@ -0,0 +1,49 @@ +expectException( MissingHashException::class ); + + do_action( + 'crontrol_cron_job', + [ + 'code' => 'echo "Hello, World!";', + ] + ); + } + + public function testInvalidHashTriggersException(): void { + $this->expectException( InvalidHashException::class ); + + do_action( + 'crontrol_cron_job', + [ + 'code' => 'echo "Hello, World!";', + 'hash' => 'invalid_hashinvalidhash', + ] + ); + } + + public function testValidPHPIsExecuted(): void { + $code = 'echo "Hello, World!";'; + + ob_start(); + + do_action( + 'crontrol_cron_job', + [ + 'code' => $code, + 'hash' => wp_hash( $code ), + ] + ); + + $output = ob_get_clean(); + + $this->assertEquals( 'Hello, World!', $output ); + } +} diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php index ae38dcec..b4164d9f 100644 --- a/tests/integration/URLEventTest.php +++ b/tests/integration/URLEventTest.php @@ -2,10 +2,21 @@ namespace Crontrol\Tests; +use Crontrol\Exception\MissingURLException; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; +use Crontrol\Exception\HTTPFailedException; class URLEventTest extends Test { + public function testMissingURLTriggersException(): void { + $this->expectException( MissingURLException::class ); + + do_action( + 'crontrol_url_cron_job', + [] + ); + } + public function testMissingHashTriggersException(): void { $this->expectException( MissingHashException::class ); @@ -31,4 +42,19 @@ public function testInvalidHashTriggersException(): void { ); } + public function testInvalidURLTriggersException(): void { + $this->expectException( HTTPFailedException::class ); + + $url = 'http://localhost:22'; + + do_action( + 'crontrol_url_cron_job', + [ + 'url' => $url, + 'method' => 'GET', + 'hash' => wp_hash( $url ), + ] + ); + } + } From 6f6e936dc020f5951952c885672a561ed1a2a57d Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 10 Oct 2025 23:40:13 +0100 Subject: [PATCH 04/16] Add an HTTP bin service for testing. --- docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 053dd15a..c177778b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,3 +2,11 @@ include: - path: vendor/johnbillion/plugin-infrastructure/tests/docker-compose.yml project_directory: . + +services: + httpbin: + image: mccutchen/go-httpbin:latest + container_name: ${COMPOSE_PROJECT_NAME}-httpbin + restart: always + environment: + PORT: 80 From d24349b44921f26c1025a43695d132f05fd39877 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 10 Oct 2025 23:40:35 +0100 Subject: [PATCH 05/16] Test tweaks. --- tests/integration/EventTimeTest.php | 4 ++-- tests/integration/PHPEventTest.php | 9 +++++---- tests/integration/URLEventTest.php | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/integration/EventTimeTest.php b/tests/integration/EventTimeTest.php index f1cf1586..115926ee 100644 --- a/tests/integration/EventTimeTest.php +++ b/tests/integration/EventTimeTest.php @@ -59,8 +59,8 @@ public function testNextRunLocalAndUTC( string $timezone_string, ?float $gmt_off $utc_time = $event->get_next_run_utc( 'Y-m-d H:i:s' ); $local_time = $event->get_next_run_local( 'Y-m-d H:i:s' ); - self::assertEquals( $expected_utc, $utc_time, 'UTC time should be correct' ); - self::assertEquals( $expected_local, $local_time, 'Local time should be correct' ); + self::assertSame( $expected_utc, $utc_time, 'UTC time should be correct' ); + self::assertSame( $expected_local, $local_time, 'Local time should be correct' ); } /** diff --git a/tests/integration/PHPEventTest.php b/tests/integration/PHPEventTest.php index 94faa09e..22f004e4 100644 --- a/tests/integration/PHPEventTest.php +++ b/tests/integration/PHPEventTest.php @@ -2,6 +2,7 @@ namespace Crontrol\Tests; +use Crontrol\Event\PHPCronEvent; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; @@ -10,7 +11,7 @@ public function testMissingHashTriggersException(): void { $this->expectException( MissingHashException::class ); do_action( - 'crontrol_cron_job', + PHPCronEvent::HOOK_NAME, [ 'code' => 'echo "Hello, World!";', ] @@ -21,7 +22,7 @@ public function testInvalidHashTriggersException(): void { $this->expectException( InvalidHashException::class ); do_action( - 'crontrol_cron_job', + PHPCronEvent::HOOK_NAME, [ 'code' => 'echo "Hello, World!";', 'hash' => 'invalid_hashinvalidhash', @@ -35,7 +36,7 @@ public function testValidPHPIsExecuted(): void { ob_start(); do_action( - 'crontrol_cron_job', + PHPCronEvent::HOOK_NAME, [ 'code' => $code, 'hash' => wp_hash( $code ), @@ -44,6 +45,6 @@ public function testValidPHPIsExecuted(): void { $output = ob_get_clean(); - $this->assertEquals( 'Hello, World!', $output ); + self::assertSame( 'Hello, World!', $output ); } } diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php index b4164d9f..ffd65c2d 100644 --- a/tests/integration/URLEventTest.php +++ b/tests/integration/URLEventTest.php @@ -2,6 +2,7 @@ namespace Crontrol\Tests; +use Crontrol\Event\URLCronEvent; use Crontrol\Exception\MissingURLException; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; @@ -12,7 +13,7 @@ public function testMissingURLTriggersException(): void { $this->expectException( MissingURLException::class ); do_action( - 'crontrol_url_cron_job', + URLCronEvent::HOOK_NAME, [] ); } @@ -21,7 +22,7 @@ public function testMissingHashTriggersException(): void { $this->expectException( MissingHashException::class ); do_action( - 'crontrol_url_cron_job', + URLCronEvent::HOOK_NAME, [ 'url' => 'http://example.com', 'method' => 'GET', @@ -33,7 +34,7 @@ public function testInvalidHashTriggersException(): void { $this->expectException( InvalidHashException::class ); do_action( - 'crontrol_url_cron_job', + URLCronEvent::HOOK_NAME, [ 'url' => 'http://example.com', 'method' => 'GET', @@ -48,7 +49,7 @@ public function testInvalidURLTriggersException(): void { $url = 'http://localhost:22'; do_action( - 'crontrol_url_cron_job', + URLCronEvent::HOOK_NAME, [ 'url' => $url, 'method' => 'GET', From f6ea02bcff31e7d5344b55947767502941130594 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 10 Oct 2025 23:41:59 +0100 Subject: [PATCH 06/16] Add URL testing with httpbin. --- tests/integration/Test.php | 26 +++++++++++++++++++++++++- tests/integration/URLEventTest.php | 14 ++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/integration/Test.php b/tests/integration/Test.php index 49e999e9..4c3687ca 100644 --- a/tests/integration/Test.php +++ b/tests/integration/Test.php @@ -2,4 +2,28 @@ namespace Crontrol\Tests; -abstract class Test extends \WP_UnitTestCase {} +abstract class Test extends \WP_UnitTestCase { + /** + * Runs the routine before each test is executed. + */ + public function set_up() { + parent::set_up(); + + /** + * Allow local httpbin requests to pass through. + * + * @param bool $external Whether HTTP request is external. + * @param string $host Host name of the requested URL. + * @param string $url Requested URL. + * @return bool Whether HTTP request is external. + */ + add_filter( 'http_request_host_is_external', static function( bool $external, string $host, string $url ): bool { + // Allow httpbin requests. + if ( 'httpbin' === $host ) { + return true; + } + + return $external; + }, 10, 3 ); + } +} diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php index ffd65c2d..06b66a32 100644 --- a/tests/integration/URLEventTest.php +++ b/tests/integration/URLEventTest.php @@ -7,6 +7,7 @@ use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; use Crontrol\Exception\HTTPFailedException; +use Crontrol\Exception\UnexpectedHTTPCodeException; class URLEventTest extends Test { public function testMissingURLTriggersException(): void { @@ -58,4 +59,17 @@ public function testInvalidURLTriggersException(): void { ); } + public function test404ResponseTriggersException(): void { + $this->expectException( UnexpectedHTTPCodeException::class ); + + $url = 'http://httpbin/status/404'; + + do_action( + URLCronEvent::HOOK_NAME, + [ + 'url' => $url, + 'hash' => wp_hash( $url ), + ] + ); + } } From cfc7e85917d793a7eec38525ff9d606c036b0c84 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 10 Oct 2025 23:59:56 +0100 Subject: [PATCH 07/16] Test the happy path too. --- phpunit.xml.dist | 1 + tests/integration/URLEventTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6aec27c6..4aa56bed 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,6 +11,7 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" + failOnRisky="true" > diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php index 06b66a32..a0a150e2 100644 --- a/tests/integration/URLEventTest.php +++ b/tests/integration/URLEventTest.php @@ -72,4 +72,29 @@ public function test404ResponseTriggersException(): void { ] ); } + + public function testSuccesfulRequestWorksAsExpected(): void { + $url = 'http://httpbin/status/200'; + + /** + * @param array|WP_Error $response HTTP response or WP_Error object. + * @param string $context Context under which the hook is fired. + * @param string $class HTTP transport used. + * @param array $parsed_args HTTP request arguments. + * @param string $url The request URL. + */ + add_action( 'http_api_debug', function( $response, $context, $class, $args, $url_called ) use ( $url ) { + self::assertSame( $url, $url_called ); + self::assertNotWPError( $response ); + self::assertSame( 200, wp_remote_retrieve_response_code( $response ) ); + }, 10, 5 ); + + do_action( + URLCronEvent::HOOK_NAME, + [ + 'url' => $url, + 'hash' => wp_hash( $url ), + ] + ); + } } From 2be6fc1309692b2842e0c35b8d9edf163394174f Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 16:15:48 +0100 Subject: [PATCH 08/16] These can all extend a Crontrol-specific runtime exception. --- src/Exception/CrontrolRuntimeException.php | 8 ++++++++ src/Exception/HTTPFailedException.php | 2 +- src/Exception/InvalidHashException.php | 2 +- src/Exception/MissingHashException.php | 2 +- src/Exception/MissingURLException.php | 2 +- src/Exception/UnexpectedHTTPCodeException.php | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/Exception/CrontrolRuntimeException.php diff --git a/src/Exception/CrontrolRuntimeException.php b/src/Exception/CrontrolRuntimeException.php new file mode 100644 index 00000000..522b8613 --- /dev/null +++ b/src/Exception/CrontrolRuntimeException.php @@ -0,0 +1,8 @@ + Date: Sat, 11 Oct 2025 16:34:33 +0100 Subject: [PATCH 09/16] Extract the URL event handling into its own function. --- src/bootstrap.php | 70 +++++++++++++++++++++--------- tests/integration/URLEventTest.php | 63 +++++++++------------------ 2 files changed, 69 insertions(+), 64 deletions(-) diff --git a/src/bootstrap.php b/src/bootstrap.php index d8685fd7..2d01d5d2 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -8,6 +8,7 @@ use Crontrol\Event\Table; use Crontrol\Event\PHPCronEvent; use Crontrol\Event\URLCronEvent; +use Crontrol\Exception\CrontrolRuntimeException; use Crontrol\Exception\MissingURLException; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; @@ -2684,28 +2685,16 @@ function json_output( $input, $pretty = true ) { * @throws HTTPFailedException * @throws UnexpectedHTTPCodeException * - * @param array $args The event args array. - * @phpstan-param array{ - * url?: string, - * name?: string, - * method?: string, - * hash?: string, - * } $args + * @param ?string $url The URL to fetch. + * @param string $method The HTTP method to use, defaults to 'GET'. + * @param ?string $hash The stored hash to check the integrity of the URL. */ -function action_url_cron_event( array $args ): void { - if ( ! url_cron_events_enabled() ) { - return; - } - - $url = $args['url'] ?? null; - $method = $args['method'] ?? 'GET'; - $hash = $args['hash'] ?? null; - +function handle_url_cron_event( $url, $method, $hash ): void { if ( empty( $url ) ) { throw new MissingURLException( esc_html( sprintf( - 'WP Crontrol: The URL is missing for a URL cron event; for more information see %s', + 'The URL is missing for a URL cron event; for more information see %s', esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), ) ) @@ -2716,7 +2705,7 @@ function action_url_cron_event( array $args ): void { throw new MissingHashException( esc_html( sprintf( - 'WP Crontrol: The stored hash is missing for a URL cron event; for more information see %s', + 'The stored hash is missing for a URL cron event; for more information see %s', esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), ) ) @@ -2728,7 +2717,7 @@ function action_url_cron_event( array $args ): void { throw new InvalidHashException( esc_html( sprintf( - 'WP Crontrol: The stored hash for a URL cron event is not valid; for more information see %s', + 'The stored hash for a URL cron event is not valid; for more information see %s', esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), ) ) @@ -2749,7 +2738,7 @@ function action_url_cron_event( array $args ): void { throw new HTTPFailedException( esc_html( sprintf( - 'WP Crontrol: Failed to fetch URL %s: %s', + 'Failed to fetch URL %s: %s', $url, $response->get_error_message() ) @@ -2764,7 +2753,7 @@ function action_url_cron_event( array $args ): void { throw new UnexpectedHTTPCodeException( esc_html( sprintf( - 'WP Crontrol: Unexpected response code for URL %s: HTTP %s %s', + 'Unexpected response code for URL %s: HTTP %s %s', $url, $code, $message @@ -2774,6 +2763,45 @@ function action_url_cron_event( array $args ): void { } } +/** + * Action handler for URL cron events. + * + * @see \Crontrol\handle_url_cron_event() + * + * @param array $args The event args array. + * @phpstan-param array{ + * url?: string, + * name?: string, + * method?: string, + * hash?: string, + * } $args + */ +function action_url_cron_event( array $args ): void { + if ( ! url_cron_events_enabled() ) { + return; + } + + $url = $args['url'] ?? null; + $method = $args['method'] ?? 'GET'; + $hash = $args['hash'] ?? null; + + try { + handle_url_cron_event( $url, $method, $hash ); + } catch ( CrontrolRuntimeException $e ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( + esc_html( + sprintf( + /* translators: %s: Message text. */ + __( 'WP Crontrol: %s', 'wp-crontrol' ), + $e->getMessage() + ), + ), + E_USER_WARNING + ); + } +} + /** * Evaluates the code in a PHP cron event using eval. * diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php index a0a150e2..81d077d2 100644 --- a/tests/integration/URLEventTest.php +++ b/tests/integration/URLEventTest.php @@ -2,79 +2,63 @@ namespace Crontrol\Tests; -use Crontrol\Event\URLCronEvent; use Crontrol\Exception\MissingURLException; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; use Crontrol\Exception\HTTPFailedException; use Crontrol\Exception\UnexpectedHTTPCodeException; +use function Crontrol\handle_url_cron_event; + class URLEventTest extends Test { public function testMissingURLTriggersException(): void { $this->expectException( MissingURLException::class ); - do_action( - URLCronEvent::HOOK_NAME, - [] - ); + $url = ''; + $hash = null; + + handle_url_cron_event( $url, 'GET', $hash ); } public function testMissingHashTriggersException(): void { $this->expectException( MissingHashException::class ); - do_action( - URLCronEvent::HOOK_NAME, - [ - 'url' => 'http://example.com', - 'method' => 'GET', - ] - ); + $url = 'http://example.com'; + $hash = null; + + handle_url_cron_event( $url, 'GET', $hash ); } public function testInvalidHashTriggersException(): void { $this->expectException( InvalidHashException::class ); - do_action( - URLCronEvent::HOOK_NAME, - [ - 'url' => 'http://example.com', - 'method' => 'GET', - 'hash' => 'invalidhash', - ] - ); + $url = 'http://example.com'; + $hash = 'invalidhash'; + + handle_url_cron_event( $url, 'GET', $hash ); } public function testInvalidURLTriggersException(): void { $this->expectException( HTTPFailedException::class ); $url = 'http://localhost:22'; + $hash = wp_hash( $url ); - do_action( - URLCronEvent::HOOK_NAME, - [ - 'url' => $url, - 'method' => 'GET', - 'hash' => wp_hash( $url ), - ] - ); + handle_url_cron_event( $url, 'GET', $hash ); } public function test404ResponseTriggersException(): void { $this->expectException( UnexpectedHTTPCodeException::class ); $url = 'http://httpbin/status/404'; + $hash = wp_hash( $url ); - do_action( - URLCronEvent::HOOK_NAME, - [ - 'url' => $url, - 'hash' => wp_hash( $url ), - ] - ); + handle_url_cron_event( $url, 'GET', $hash ); } public function testSuccesfulRequestWorksAsExpected(): void { $url = 'http://httpbin/status/200'; + $hash = wp_hash( $url ); /** * @param array|WP_Error $response HTTP response or WP_Error object. @@ -86,15 +70,8 @@ public function testSuccesfulRequestWorksAsExpected(): void { add_action( 'http_api_debug', function( $response, $context, $class, $args, $url_called ) use ( $url ) { self::assertSame( $url, $url_called ); self::assertNotWPError( $response ); - self::assertSame( 200, wp_remote_retrieve_response_code( $response ) ); }, 10, 5 ); - do_action( - URLCronEvent::HOOK_NAME, - [ - 'url' => $url, - 'hash' => wp_hash( $url ), - ] - ); + handle_url_cron_event( $url, 'GET', $hash ); } } From 1a9d67c3c0bcc026e901bb13332bbc4cec20fce5 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 16:46:36 +0100 Subject: [PATCH 10/16] Extract the PHP event handling into its own function. --- src/bootstrap.php | 68 +++++++++++++++++++++--------- tests/integration/PHPEventTest.php | 32 +++++--------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/bootstrap.php b/src/bootstrap.php index 2d01d5d2..5f5b4906 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -2834,6 +2834,43 @@ function action_url_cron_event( array $args ): void { * @throws MissingHashException * @throws InvalidHashException * + * @param ?string $code The PHP code to evaluate. + * @param ?string $hash The stored hash to check the integrity of the PHP code. + */ +function handle_php_cron_event( $code, $hash ): void { + if ( empty( $hash ) ) { + throw new MissingHashException( + esc_html( + sprintf( + 'The stored hash is missing for a PHP cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + ), + ) + ); + } + + // Check the integrity of the PHP code. + if ( ! check_integrity( $code, $hash ) ) { + throw new InvalidHashException( + esc_html( + sprintf( + 'The stored hash for a PHP cron event is not valid; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + ), + ) + ); + } + + // Please see the function description above for information about the safety of this code. + // phpcs:ignore Squiz.PHP.Eval.Discouraged + eval( $code ); +} + +/** + * Action handler for PHP cron events. + * + * @see \Crontrol\handle_php_cron_event() + * * @param array|string $args The event args array, or a string containing the PHP code to evaluate. * @phpstan-param array{ * code?: string, @@ -2859,32 +2896,21 @@ function action_php_cron_event( $args ): void { $hash = $args['hash'] ?? null; } - if ( empty( $hash ) ) { - throw new MissingHashException( - esc_html( - sprintf( - 'WP Crontrol: The stored hash is missing for a PHP cron event; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), - ), - ) - ); - } - - // Check the integrity of the PHP code. - if ( ! check_integrity( $code, $hash ) ) { - throw new InvalidHashException( + try { + handle_php_cron_event( $code, $hash ); + } catch ( CrontrolRuntimeException $e ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( esc_html( sprintf( - 'WP Crontrol: The stored hash for a PHP cron event is not valid; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + /* translators: %s: Message text. */ + __( 'WP Crontrol: %s', 'wp-crontrol' ), + $e->getMessage() ), - ) + ), + E_USER_WARNING ); } - - // Please see the function description above for information about the safety of this code. - // phpcs:ignore Squiz.PHP.Eval.Discouraged - eval( $code ); } /** diff --git a/tests/integration/PHPEventTest.php b/tests/integration/PHPEventTest.php index 22f004e4..6f961ca7 100644 --- a/tests/integration/PHPEventTest.php +++ b/tests/integration/PHPEventTest.php @@ -2,46 +2,36 @@ namespace Crontrol\Tests; -use Crontrol\Event\PHPCronEvent; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; +use function Crontrol\handle_php_cron_event; + class PHPEventTest extends Test { public function testMissingHashTriggersException(): void { $this->expectException( MissingHashException::class ); - do_action( - PHPCronEvent::HOOK_NAME, - [ - 'code' => 'echo "Hello, World!";', - ] - ); + $code = 'echo "Hello, World!";'; + + handle_php_cron_event( $code, null ); } public function testInvalidHashTriggersException(): void { $this->expectException( InvalidHashException::class ); - do_action( - PHPCronEvent::HOOK_NAME, - [ - 'code' => 'echo "Hello, World!";', - 'hash' => 'invalid_hashinvalidhash', - ] - ); + $code = 'echo "Hello, World!";'; + $hash = 'invalidhash'; + + handle_php_cron_event( $code, $hash ); } public function testValidPHPIsExecuted(): void { $code = 'echo "Hello, World!";'; + $hash = wp_hash( $code ); ob_start(); - do_action( - PHPCronEvent::HOOK_NAME, - [ - 'code' => $code, - 'hash' => wp_hash( $code ), - ] - ); + handle_php_cron_event( $code, $hash ); $output = ob_get_clean(); From 6ce171c48e281b351f1ccb88ee8430e114354fad Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 17:43:26 +0100 Subject: [PATCH 11/16] Validate the URL prior to fetching it so a clearer error message is logged when the event runs. --- src/Exception/InvalidURLException.php | 8 ++++++++ src/bootstrap.php | 19 ++++++++++++------- src/event.php | 5 +++-- tests/integration/URLEventTest.php | 4 ++-- 4 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 src/Exception/InvalidURLException.php diff --git a/src/Exception/InvalidURLException.php b/src/Exception/InvalidURLException.php new file mode 100644 index 00000000..ebae9500 --- /dev/null +++ b/src/Exception/InvalidURLException.php @@ -0,0 +1,8 @@ + 30, 'method' => $method, @@ -2831,8 +2836,8 @@ function action_url_cron_event( array $args ): void { * * @link https://wp-crontrol.com/docs/php-cron-events/ * - * @throws MissingHashException - * @throws InvalidHashException + * @throws \Crontrol\Exception\MissingHashException + * @throws \Crontrol\Exception\InvalidHashException * * @param ?string $code The PHP code to evaluate. * @param ?string $hash The stored hash to check the integrity of the PHP code. diff --git a/src/event.php b/src/event.php index 6109cecb..343bb866 100644 --- a/src/event.php +++ b/src/event.php @@ -5,6 +5,7 @@ namespace Crontrol\Event; +use Crontrol\Exception\InvalidURLException; use WP_Error; use InvalidArgumentException; @@ -497,7 +498,7 @@ function get_core_cron_array() { * * @see https://github.com/WordPress/wordpress-develop/blob/197f0a71ad27d0688b6380c869aeaf92addd1451/src/wp-includes/class-wp-http.php#L283-L299 * - * @throws InvalidArgumentException If the URL is not valid or contains an invalid protocol. + * @throws \Crontrol\Exception\InvalidURLException If the URL is invalid or not allowed. * * @param string $url The URL to validate. */ @@ -505,7 +506,7 @@ function validate_url( string $url ): void { $valid = wp_http_validate_url( $url ); if ( $valid === false ) { - throw new InvalidArgumentException( + throw new InvalidURLException( esc_html( sprintf( /* translators: %s: The URL that failed validation. */ diff --git a/tests/integration/URLEventTest.php b/tests/integration/URLEventTest.php index 81d077d2..a19bbc14 100644 --- a/tests/integration/URLEventTest.php +++ b/tests/integration/URLEventTest.php @@ -5,7 +5,7 @@ use Crontrol\Exception\MissingURLException; use Crontrol\Exception\MissingHashException; use Crontrol\Exception\InvalidHashException; -use Crontrol\Exception\HTTPFailedException; +use Crontrol\Exception\InvalidURLException; use Crontrol\Exception\UnexpectedHTTPCodeException; use function Crontrol\handle_url_cron_event; @@ -39,7 +39,7 @@ public function testInvalidHashTriggersException(): void { } public function testInvalidURLTriggersException(): void { - $this->expectException( HTTPFailedException::class ); + $this->expectException( InvalidURLException::class ); $url = 'http://localhost:22'; $hash = wp_hash( $url ); From 0afccd9dd7ef1fab8b38b4bc9b11acea430d8435 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 17:44:38 +0100 Subject: [PATCH 12/16] This is a useless check because it gets caught by `wp_http_validate_url()` with a narrower list of protocols. --- src/event.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/event.php b/src/event.php index 343bb866..80745b96 100644 --- a/src/event.php +++ b/src/event.php @@ -516,18 +516,4 @@ function validate_url( string $url ): void { ) ); } - - $filtered = wp_kses_bad_protocol( $url, array( 'http', 'https', 'ssl' ) ); - - if ( $filtered === '' ) { - throw new InvalidArgumentException( - esc_html( - sprintf( - /* translators: %s: The URL that failed validation. */ - __( 'The URL "%s" contains an invalid protocol', 'wp-crontrol' ), - $url, - ) - ) - ); - } } From 52482df277174e47ec828e36ca352e0d288ed359 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 17:57:40 +0100 Subject: [PATCH 13/16] Add a more specific exception for an unknown schedule. --- src/Event/Event.php | 5 +++-- src/Exception/UnknownScheduleException.php | 8 ++++++++ src/bootstrap.php | 4 ++-- src/event-list-table.php | 5 +++-- tests/integration/EventScheduleTest.php | 3 ++- 5 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/Exception/UnknownScheduleException.php diff --git a/src/Event/Event.php b/src/Event/Event.php index ba4b0bd2..66c395dd 100644 --- a/src/Event/Event.php +++ b/src/Event/Event.php @@ -10,6 +10,7 @@ use Crontrol\Event\CoreCronEvent; use Crontrol\Event\ActionSchedulerEvent; use Crontrol\Event\StandardEvent; +use Crontrol\Exception\UnknownScheduleException; /** * Base class for cron events. @@ -278,7 +279,7 @@ public function is_persistent_core_hook(): bool { * Get the schedule name for this event. * * @return string The schedule display name. - * @throws \RuntimeException If schedule is unknown. + * @throws UnknownScheduleException If schedule is unknown. */ public function get_schedule_name(): string { if ( ! $this->is_recurring() ) { @@ -291,7 +292,7 @@ public function get_schedule_name(): string { return isset( $schedules[ $this->schedule ]['display'] ) ? $schedules[ $this->schedule ]['display'] : $schedules[ $this->schedule ]['name']; } - throw new \RuntimeException( + throw new UnknownScheduleException( esc_html( sprintf( /* translators: %s: Schedule name */ diff --git a/src/Exception/UnknownScheduleException.php b/src/Exception/UnknownScheduleException.php new file mode 100644 index 00000000..d7d25a1d --- /dev/null +++ b/src/Exception/UnknownScheduleException.php @@ -0,0 +1,8 @@ +get_schedule_name(); - } catch ( \RuntimeException $e ) { + } catch ( UnknownScheduleException $e ) { $schedule_name = $e->getMessage(); } diff --git a/src/event-list-table.php b/src/event-list-table.php index 20ea6703..e129f6cf 100644 --- a/src/event-list-table.php +++ b/src/event-list-table.php @@ -5,6 +5,7 @@ namespace Crontrol\Event; +use Crontrol\Exception\UnknownScheduleException; use DateTimeImmutable; use function Crontrol\php_cron_events_enabled; @@ -362,7 +363,7 @@ public function single_row( $event ) { try { $schedule_name = $event->get_schedule_name(); - } catch ( \RuntimeException $e ) { + } catch ( UnknownScheduleException $e ) { $classes[] = 'crontrol-error'; } @@ -888,7 +889,7 @@ protected function column_crontrol_next( Event $event ): string { protected function column_crontrol_schedule( Event $event ): string { try { $schedule_name = $event->get_schedule_name(); - } catch ( \RuntimeException $e ) { + } catch ( UnknownScheduleException $e ) { return sprintf( ' %s', esc_html( $e->getMessage() ) diff --git a/tests/integration/EventScheduleTest.php b/tests/integration/EventScheduleTest.php index 510ee8d2..69831b18 100644 --- a/tests/integration/EventScheduleTest.php +++ b/tests/integration/EventScheduleTest.php @@ -3,13 +3,14 @@ namespace Crontrol\Tests; use Crontrol\Event\Event; +use Crontrol\Exception\UnknownScheduleException; class EventScheduleTest extends Test { /** * @covers \Crontrol\Event\Event::get_schedule_name */ public function testGetScheduleNameHandlesUnknownSchedule(): void { - self::expectException(\RuntimeException::class); + self::expectException( UnknownScheduleException::class ); $timestamp = time() + 3600; From b4a00c34ce92a158e6fdcd10fa69cd7f9b3eecd2 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 17:57:53 +0100 Subject: [PATCH 14/16] Tweaks. --- src/bootstrap.php | 1 - src/event.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bootstrap.php b/src/bootstrap.php index e3a7f989..8a25480e 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -2683,7 +2683,6 @@ function json_output( $input, $pretty = true ) { * @throws \Crontrol\Exception\MissingURLException * @throws \Crontrol\Exception\MissingHashException * @throws \Crontrol\Exception\InvalidHashException - * @throws \Crontrol\Exception\InvalidURLException * @throws \Crontrol\Exception\HTTPFailedException * @throws \Crontrol\Exception\UnexpectedHTTPCodeException * diff --git a/src/event.php b/src/event.php index 80745b96..d89be90b 100644 --- a/src/event.php +++ b/src/event.php @@ -160,7 +160,7 @@ function add( $next_run_local, $schedule, $hook, array $args ) { if ( URLCronEvent::HOOK_NAME === $hook && ! empty( $args[0]['url'] ) ) { try { validate_url( $args[0]['url'] ); - } catch ( InvalidArgumentException $e ) { + } catch ( InvalidURLException $e ) { $args[0]['url_error_message'] = $e->getMessage(); $error = $e; } From 1e7ff0ea7b8630b36176282369fc7fcaca6f807c Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 18:19:33 +0100 Subject: [PATCH 15/16] Remove unnecessary escaping from exception messages. --- phpcs.xml.dist | 3 +++ src/Event/Event.php | 10 +++---- src/bootstrap.php | 66 ++++++++++++++++++--------------------------- src/event.php | 10 +++---- 4 files changed, 37 insertions(+), 52 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index d9c25e5a..3ac587b9 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -39,6 +39,9 @@ + + + diff --git a/src/Event/Event.php b/src/Event/Event.php index 66c395dd..28309001 100644 --- a/src/Event/Event.php +++ b/src/Event/Event.php @@ -293,12 +293,10 @@ public function get_schedule_name(): string { } throw new UnknownScheduleException( - esc_html( - sprintf( - /* translators: %s: Schedule name */ - __( 'Unknown (%s)', 'wp-crontrol' ), - $this->schedule - ) + sprintf( + /* translators: %s: Schedule name */ + __( 'Unknown (%s)', 'wp-crontrol' ), + $this->schedule ) ); } diff --git a/src/bootstrap.php b/src/bootstrap.php index 8a25480e..cf0720a9 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -2693,22 +2693,18 @@ function json_output( $input, $pretty = true ) { function handle_url_cron_event( $url, $method, $hash ): void { if ( empty( $url ) ) { throw new MissingURLException( - esc_html( - sprintf( - 'The URL is missing for a URL cron event; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), - ) + sprintf( + 'The URL is missing for a URL cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), ) ); } if ( empty( $hash ) ) { throw new MissingHashException( - esc_html( - sprintf( - 'The stored hash is missing for a URL cron event; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), - ) + sprintf( + 'The stored hash is missing for a URL cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), ) ); } @@ -2716,11 +2712,9 @@ function handle_url_cron_event( $url, $method, $hash ): void { // Check the integrity of the URL. if ( ! check_integrity( $url, $hash ) ) { throw new InvalidHashException( - esc_html( - sprintf( - 'The stored hash for a URL cron event is not valid; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), - ) + sprintf( + 'The stored hash for a URL cron event is not valid; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=url' ) ), ) ); } @@ -2740,12 +2734,10 @@ function handle_url_cron_event( $url, $method, $hash ): void { if ( is_wp_error( $response ) ) { throw new HTTPFailedException( - esc_html( - sprintf( - 'Failed to fetch URL %s: %s', - $url, - $response->get_error_message() - ) + sprintf( + 'Failed to fetch URL %s: %s', + $url, + $response->get_error_message() ) ); } @@ -2755,13 +2747,11 @@ function handle_url_cron_event( $url, $method, $hash ): void { if ( $code < 200 || $code >= 300 ) { throw new UnexpectedHTTPCodeException( - esc_html( - sprintf( - 'Unexpected response code for URL %s: HTTP %s %s', - $url, - $code, - $message - ) + sprintf( + 'Unexpected response code for URL %s: HTTP %s %s', + $url, + $code, + $message ) ); } @@ -2844,24 +2834,20 @@ function action_url_cron_event( array $args ): void { function handle_php_cron_event( $code, $hash ): void { if ( empty( $hash ) ) { throw new MissingHashException( - esc_html( - sprintf( - 'The stored hash is missing for a PHP cron event; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), - ), - ) + sprintf( + 'The stored hash is missing for a PHP cron event; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + ), ); } // Check the integrity of the PHP code. if ( ! check_integrity( $code, $hash ) ) { throw new InvalidHashException( - esc_html( - sprintf( - 'The stored hash for a PHP cron event is not valid; for more information see %s', - esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), - ), - ) + sprintf( + 'The stored hash for a PHP cron event is not valid; for more information see %s', + esc_url_raw( admin_url( 'tools.php?page=wp-crontrol&crontrol_hooks_type=php' ) ), + ), ); } diff --git a/src/event.php b/src/event.php index d89be90b..4a7abca2 100644 --- a/src/event.php +++ b/src/event.php @@ -507,12 +507,10 @@ function validate_url( string $url ): void { if ( $valid === false ) { throw new InvalidURLException( - esc_html( - sprintf( - /* translators: %s: The URL that failed validation. */ - __( 'The URL "%s" is not allowed', 'wp-crontrol' ), - $url, - ) + sprintf( + /* translators: %s: The URL that failed validation. */ + __( 'The URL "%s" is not allowed', 'wp-crontrol' ), + $url, ) ); } From 9e95488e11a273e4f70f35ebcb3cfe67026e7eb5 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 11 Oct 2025 18:36:05 +0100 Subject: [PATCH 16/16] These errors don't need to be escaped as they're called within the context of a cron event, not a regular web request. --- src/bootstrap.php | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/bootstrap.php b/src/bootstrap.php index cf0720a9..fd4e551e 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -2782,17 +2782,16 @@ function action_url_cron_event( array $args ): void { try { handle_url_cron_event( $url, $method, $hash ); } catch ( CrontrolRuntimeException $e ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped trigger_error( - esc_html( - sprintf( - /* translators: %s: Message text. */ - __( 'WP Crontrol: %s', 'wp-crontrol' ), - $e->getMessage() - ), + sprintf( + /* translators: %s: Message text. */ + __( 'WP Crontrol: %s', 'wp-crontrol' ), + $e->getMessage() ), E_USER_WARNING ); + // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped } } @@ -2889,17 +2888,16 @@ function action_php_cron_event( $args ): void { try { handle_php_cron_event( $code, $hash ); } catch ( CrontrolRuntimeException $e ) { - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped trigger_error( - esc_html( - sprintf( - /* translators: %s: Message text. */ - __( 'WP Crontrol: %s', 'wp-crontrol' ), - $e->getMessage() - ), + sprintf( + /* translators: %s: Message text. */ + __( 'WP Crontrol: %s', 'wp-crontrol' ), + $e->getMessage() ), E_USER_WARNING ); + // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped } }