diff --git a/.gitignore b/.gitignore index e25beaf5..301b8bf1 100755 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ cypress/videos/* cypress/screenshots/* cypress/integration/localhost* cypress/fixtures -cypress.env.json \ No newline at end of file +cypress.env.json +.phpunit.result.cache diff --git a/includes/admin/feedzy-rss-feeds-import.php b/includes/admin/feedzy-rss-feeds-import.php index f9e036d5..eae0551a 100644 --- a/includes/admin/feedzy-rss-feeds-import.php +++ b/includes/admin/feedzy-rss-feeds-import.php @@ -1996,6 +1996,55 @@ private function get_file_type_by_url( $url ) { return $content_type; } + /** + * Will escape the provided URL and convert it to ASCII. + * + * @param string $url The URL to convert. + * + * @return string + */ + private function convert_url_to_ascii( $url ) { + $parts = wp_parse_url( $url ); + if ( empty( $parts ) ) { + return esc_url( $url ); + } + + $scheme = ''; + if ( isset( $parts['scheme'] ) ) { + $scheme = $parts['scheme'] . '://'; + } + + $host = ''; + if ( isset( $parts['scheme'] ) ) { + $host = idn_to_ascii( $parts['host'], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 ); + } + + $url = $scheme . $host; + if ( isset( $parts['port'] ) ) { + $url .= ':' . $parts['port']; + } + if ( isset( $parts['path'] ) ) { + $ascii_path = ''; + $path = $parts['path']; + $len = strlen( $path ); + for ( $i = 0; $i < $len; $i++ ) { + if ( preg_match( '/^[A-Za-z0-9\/?=+%_.~-]$/', $path[ $i ] ) ) { + $ascii_path .= $path[ $i ]; + } else { + $ascii_path .= rawurlencode( $path[ $i ] ); + } + } + $url .= $ascii_path; + } + if ( isset( $parts['query'] ) ) { + $url .= '?' . $parts['query']; + } + if ( isset( $parts['fragment'] ) ) { + $url .= '#' . $parts['fragment']; + } + return esc_url( $url ); + } + /** * Downloads and sets a post featured image if possible. * @@ -2018,8 +2067,10 @@ private function try_save_featured_image( $img_source_url, $post_id, $post_title if ( ! $id ) { - // We escape the URL to ensure that valid URLs are passed by the filter. - if ( filter_var( esc_url( $img_source_url ), FILTER_VALIDATE_URL ) === false ) { + // We escape the URL to ensure that valid URLs are passed by the filter. We also convert the URL parts to ASCII. + // This is necessary because FILTER_VALIDATE_URL only validates against ASCII URLs. + $escaped_url = $this->convert_url_to_ascii( $img_source_url ); + if ( filter_var( $escaped_url, FILTER_VALIDATE_URL ) === false ) { $import_errors[] = 'Invalid Featured Image URL: ' . $img_source_url; return false; } diff --git a/tests/test-image-import.php b/tests/test-image-import.php index 37616005..85e3ee93 100644 --- a/tests/test-image-import.php +++ b/tests/test-image-import.php @@ -59,4 +59,23 @@ public function test_image_import_url() { $this->assertFalse( $response ); $this->assertTrue( empty( $import_errors ) ); } + + public function test_import_image_special_characters() { + $feedzy = new Feedzy_Rss_Feeds_Import( 'feedzy-rss-feeds', '1.2.0' ); + + $reflector = new ReflectionClass( $feedzy ); + $try_save_featured_image = $reflector->getMethod( 'try_save_featured_image' ); + $try_save_featured_image->setAccessible( true ); + + $import_errors = array(); + $import_info = array(); + + $arguments = array( 'https://example.com/path_to_image/çöp.jpg?itok=ZYU_ihPB', 0, 'Post Title', &$import_errors, &$import_info, array() ); + $response = $try_save_featured_image->invokeArgs( $feedzy, $arguments ); + + // expected response is false because the image does not exist, but the URL is valid so no $import_errors should be set. + $this->assertFalse( $response ); + $this->assertTrue( empty( $import_errors ) ); + + } }