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
+ );
+ }
+}