diff --git a/includes/helper-functions.php b/includes/helper-functions.php index a048b5de..61b32469 100644 --- a/includes/helper-functions.php +++ b/includes/helper-functions.php @@ -104,7 +104,7 @@ function edac_ordinal( $number ) { NumberFormatter::ORDINAL ) )->format( $number ); - + } else { if ( $number % 100 >= 11 && $number % 100 <= 13 ) { $ordinal = $number . 'th'; @@ -125,7 +125,7 @@ function edac_ordinal( $number ) { } } return $ordinal; - + } } @@ -857,3 +857,97 @@ function edac_database_table_count( $table ) { return $count; } + +/** + * Add a scheme to file it looks like a url but doesn't have one. + * + * @since 1.10.1 + * + * @param string $file The filename. + * @param string $site_protocol The site protocol. Default is 'https'. + * + * @return string The filename unchanged if it doesn't look like a URL, or with a scheme added if it does. + */ +function edac_url_add_scheme_if_not_existing( string $file, string $site_protocol = '' ): string { + + // if it starts with some valid scheme return unchanged. + $valid_schemes = array( 'http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'file', 'data', 'irc', 'ssh', 'sftp' ); + $start_of_file = substr( $file, 0, 6 ); + foreach ( $valid_schemes as $scheme ) { + if ( str_starts_with( $start_of_file, $scheme ) ) { + return $file; + } + } + + // if it starts with / followed by any alphanumeric assume it's a relative url. + if ( preg_match( '/^\/[a-zA-Z0-9]/', $file ) ) { + return $file; + } + + // by this point it doesn't seem like a url or a relative path so make it into one. + $file_location = ltrim( $file, '/' ); + $site_scheme = ( ! empty( $site_protocol ) ) + ? $site_protocol + : ( is_ssl() ? 'https' : 'http' ); + + return "{$site_scheme}://{$file_location}"; +} + +/** + * Requests the headers of a URL to check if it exists. + * + * @since 1.10.1 + * + * @param string $url the url to check. + * @return bool + */ +function edac_url_exists( string $url ): bool { + + $response = wp_remote_head( $url ); + + if ( + is_wp_error( $response ) || + ( // Check if the response code is not in the 2xx range. + wp_remote_retrieve_response_code( $response ) < 200 || + wp_remote_retrieve_response_code( $response ) > 299 + ) + ) { + return false; + } + + return true; +} + +/** + * Get a file from local or remote source as a binary file handle. + * + * @since 1.10.1 + * + * @param string $filename The file location, either local or a remote URL. + * @return resource|bool The file binary string or false if the file could not be opened. + */ +function edac_get_file_opened_as_binary( string $filename ) { + if ( + str_starts_with( $filename, 'http' ) || + preg_match( '/^\/[a-zA-Z0-9]/', $filename ) + ) { + $file = $filename; + } else { + $file = edac_url_add_scheme_if_not_existing( $filename ); + $url_exists = edac_url_exists( $file ); + } + + // if this url doesn't exist, return false. + if ( isset( $url_exists ) && false === $url_exists ) { + return false; + } + + try { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- path validated above. + $fh = fopen( $file, 'rb' ); + } catch ( Exception $e ) { + return false; + } + + return $fh; +} diff --git a/includes/rules/img_animated_gif.php b/includes/rules/img_animated_gif.php index 2a20b01a..b6b908fd 100644 --- a/includes/rules/img_animated_gif.php +++ b/includes/rules/img_animated_gif.php @@ -69,31 +69,14 @@ function edac_rule_img_animated_gif( $content, $post ) { // phpcs:ignore -- $pos } /** - * Checks if a gif image is anaimated + * Checks if a gif image is animated * * @param string $filename The filename. * @return bool */ -function edac_img_gif_is_animated( $filename ) { +function edac_img_gif_is_animated( string $filename ): bool { - $upload_dir_info = wp_get_upload_dir(); - $uploads_base_dir = trailingslashit( $upload_dir_info['basedir'] ); - - // Get WordPress base URL and remove it from the filename to get the relative path. - $base_url = trailingslashit( get_site_url() ); - $relative_path = str_replace( $base_url, '', $filename ); - - // Construct the full file system path. - $file_system_path = $uploads_base_dir . $relative_path; - - // Check if the file is within the WordPress uploads directory. - if ( 0 !== strpos( $file_system_path, $uploads_base_dir ) ) { - return false; - } - - // First, attempt to open the file. - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- path validated above. - $fh = fopen( $filename, 'rb' ); + $fh = edac_get_file_opened_as_binary( $filename ); // Then, check if the file handle is false, indicating an error. if ( false === $fh ) { @@ -133,24 +116,7 @@ function edac_img_gif_is_animated( $filename ) { */ function edac_img_webp_is_animated( $filename ) { - $upload_dir_info = wp_get_upload_dir(); - $uploads_base_dir = trailingslashit( $upload_dir_info['basedir'] ); - - // Get WordPress base URL and remove it from the filename to get the relative path. - $base_url = trailingslashit( get_site_url() ); - $relative_path = str_replace( $base_url, '', $filename ); - - // Construct the full file system path. - $file_system_path = $uploads_base_dir . $relative_path; - - // Check if the file is within the WordPress uploads directory. - if ( 0 !== strpos( $file_system_path, $uploads_base_dir ) ) { - return false; - } - - // First, attempt to open the file. - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- path validated above. - $fh = fopen( $filename, 'rb' ); + $fh = edac_get_file_opened_as_binary( $filename ); // Then, check if the file handle is false, indicating an error. if ( false === $fh ) { diff --git a/tests/assets/animated.gif b/tests/assets/animated.gif new file mode 100644 index 00000000..c76b6f6b Binary files /dev/null and b/tests/assets/animated.gif differ diff --git a/tests/assets/animated.webp b/tests/assets/animated.webp new file mode 100644 index 00000000..d36261fa Binary files /dev/null and b/tests/assets/animated.webp differ diff --git a/tests/assets/static.gif b/tests/assets/static.gif new file mode 100644 index 00000000..06107759 Binary files /dev/null and b/tests/assets/static.gif differ diff --git a/tests/assets/static.webp b/tests/assets/static.webp new file mode 100644 index 00000000..9d6f0af4 Binary files /dev/null and b/tests/assets/static.webp differ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9b080e7e..e5e2cf00 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -35,3 +35,5 @@ function _manually_load_plugin() { // Start up the WP testing environment. require "{$_tests_dir}/includes/bootstrap.php"; + +define( 'EDAC_TEST_ASSETS_DIR', plugin_dir_path( __FILE__ ) . 'assets/' ); diff --git a/tests/phpunit/helper-functions/GetFileOpenedAsBinaryTest.php b/tests/phpunit/helper-functions/GetFileOpenedAsBinaryTest.php new file mode 100644 index 00000000..02214f37 --- /dev/null +++ b/tests/phpunit/helper-functions/GetFileOpenedAsBinaryTest.php @@ -0,0 +1,41 @@ +assertNotFalse( $fh ); + fclose( $fh ); + } + + /** + * Test if file is not opened as binary. + * + * @group external-http + */ + public function test_file_not_opened_as_binary() { + $file = 'https://httpbin.org/status/404'; + + $fh = edac_get_file_opened_as_binary( $file ); + + $this->assertFalse( $fh ); + } +} diff --git a/tests/phpunit/helper-functions/UrlAddSchemeIfNotExistingTest.php b/tests/phpunit/helper-functions/UrlAddSchemeIfNotExistingTest.php new file mode 100644 index 00000000..9766044b --- /dev/null +++ b/tests/phpunit/helper-functions/UrlAddSchemeIfNotExistingTest.php @@ -0,0 +1,62 @@ +assertEquals( $expected, edac_url_add_scheme_if_not_existing( $url, 'https' ) ); + + $url_http = '//example.com'; + $expected = 'http://example.com'; + $this->assertEquals( $expected, edac_url_add_scheme_if_not_existing( $url_http, 'http' ) ); + + $url_one_slash = '/example.com'; + $this->assertEquals( $url_one_slash, edac_url_add_scheme_if_not_existing( $url_one_slash ) ); + + $url_no_slash = 'example.com'; + $this->assertStringStartsWith( 'http', edac_url_add_scheme_if_not_existing( $url_no_slash ) ); + } + + /** + * Test that the function doesn't add a scheme to a url that already has one. + */ + public function test_url_add_scheme_if_not_existing_with_scheme() { + $url = 'https://example.com'; + $this->assertEquals( $url, edac_url_add_scheme_if_not_existing( $url ) ); + + $url_http = 'http://example.com/'; + $this->assertEquals( $url_http, edac_url_add_scheme_if_not_existing( $url_http, 'http' ) ); + } + + /** + * Test that the function doesn't add a scheme to a url that already has one, even when not http*. + */ + public function test_url_add_scheme_if_not_existing_unmoidfied_with_ftp() { + $ftp_url = 'ftp://example.com'; + $this->assertEquals( $ftp_url, edac_url_add_scheme_if_not_existing( $ftp_url ) ); + } + + /** + * Test that the function doesn't add a scheme to a local or relative url. + */ + public function test_url_add_scheme_if_not_existing_unmoidfied_when_local_or_relative() { + $local_path = '/wp-content/uploads/2024/03/image.gif'; + $this->assertEquals( $local_path, edac_url_add_scheme_if_not_existing( $local_path ) ); + + $relative_url = '/about.gif'; + $this->assertEquals( $relative_url, edac_url_add_scheme_if_not_existing( $relative_url ) ); + } +} diff --git a/tests/phpunit/helper-functions/UrlExistsTest.php b/tests/phpunit/helper-functions/UrlExistsTest.php new file mode 100644 index 00000000..e2bda1a1 --- /dev/null +++ b/tests/phpunit/helper-functions/UrlExistsTest.php @@ -0,0 +1,34 @@ +assertTrue( edac_url_exists( $url ) ); + } + + /** + * Test that we get false on a non-2xx status code. + * + * @group external-http + */ + public function test_url_does_not_exist() { + $url = 'https://httpbin.org/status/404'; + $this->assertFalse( edac_url_exists( $url ) ); + $url = 'https://httpbin.org/status/418'; + $this->assertFalse( edac_url_exists( $url ) ); + } +} diff --git a/tests/phpunit/includes/rules/ImgAnimatedGifTest.php b/tests/phpunit/includes/rules/ImgAnimatedGifTest.php new file mode 100644 index 00000000..a10cf7e5 --- /dev/null +++ b/tests/phpunit/includes/rules/ImgAnimatedGifTest.php @@ -0,0 +1,114 @@ +assertTrue( edac_img_gif_is_animated( $filename ) ); + } + + /** + * Test that static gif is not detected as animated. + */ + public function testAnimatedGifDetectionWithStaticGif() { + $filename = EDAC_TEST_ASSETS_DIR . 'static.gif'; + $this->assertFalse( edac_img_gif_is_animated( $filename ) ); + } + + /** + * Test that animated webp is detected as animated. + */ + public function testAnimatedWebpDetectionWithAnimatedWebp() { + $filename = EDAC_TEST_ASSETS_DIR . 'animated.webp'; + $this->assertTrue( edac_img_webp_is_animated( $filename ) ); + } + + /** + * Test that static webp is not detected as animated. + */ + public function testAnimatedWebpDetectionWithStaticWebp() { + $filename = EDAC_TEST_ASSETS_DIR . 'static.webp'; + $this->assertFalse( edac_img_webp_is_animated( $filename ) ); + } + + /** + * Test that animated gif is detected when passed through entire rule. + */ + public function testRuleWithAnimatedGifInContent() { + $html = ''; + $dom = $this->get_DOM( $html ); + $content = array( 'html' => $dom ); + $post = new stdClass(); + $errors = edac_rule_img_animated_gif( $content, $post ); + $this->assertNotEmpty( $errors ); + } + + /** + * Test that static gif is not detected as animated when passed through entire rule. + */ + public function testRuleWithStaticGifInContent() { + $html = ''; + $dom = $this->get_DOM( $html ); + $content = array( 'html' => $dom ); + $post = new stdClass(); + $errors = edac_rule_img_animated_gif( $content, $post ); + $this->assertEmpty( $errors ); + } + + /** + * Test that animated webp is detected as animated when passed through entire rule. + */ + public function testRuleWithAnimatedWebpInContent() { + $html = ''; + $dom = $this->get_DOM( $html ); + $content = array( 'html' => $dom ); + $post = new stdClass(); + $errors = edac_rule_img_animated_gif( $content, $post ); + $this->assertNotEmpty( $errors ); + } + + /** + * Test that static webp is not detected as animated when passed through entire rule. + */ + public function testRuleWithStaticWebpInContent() { + + $html = ''; + $dom = $this->get_DOM( $html ); + $content = array( 'html' => $dom ); + $post = new stdClass(); + $errors = edac_rule_img_animated_gif( $content, $post ); + $this->assertEmpty( $errors ); + } + + /** + * Wrapper to generate dom objects that match the shape of the object in the plugin. + * + * @param string $html_string HTML string. + * @return EDAC_Dom + */ + private function get_DOM( string $html_string = '' ) { + return new EDAC_Dom( + $html_string, + true, + true, + DEFAULT_TARGET_CHARSET, + true, + DEFAULT_BR_TEXT, + DEFAULT_SPAN_TEXT + ); + } +}