From 43efeee7012706829bf37aaa91a851025c25ddb3 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Wed, 29 Jul 2026 10:48:20 +1000 Subject: [PATCH] feat: support font URLs in GPDFAPI::add_pdf_font() Each of the four font keys now accepts an http(s) URL as well as an absolute server path, and the two can be mixed within the one call. Downloads go through the new RemoteFontDownloader, which streams the response to a temp file with wp_remote_get(). The URL is put through wp_http_validate_url() *before* the request rather than relying solely on reject_unsafe_urls: WP applies that flag after the pre_http_request short-circuit, so a plugin filtering that hook would otherwise bypass the SSRF check entirely. reject_unsafe_urls is still passed so each redirect hop is revalidated. Anything with a scheme is routed to the downloader (and rejected unless it is http/https) instead of falling through to the local-path branch, because is_file('file:///etc/passwd') returns true. limit_response_size is set to MAX_FILE_SIZE + 1 rather than MAX_FILE_SIZE so a truncated oversized response always trips the post-download size check; capping at exactly the limit would let a silently-truncated font through as if it were complete. Content is still proven to be a real TTF by the existing TtfFontValidation before it reaches the font directory. The synthetic $_FILES entry drops the 'file' and 'size' keys - the vendored upload library reads only tmp_name/name/error, so 'file' was loading every font fully into memory for nothing. Co-Authored-By: Claude Opus 5 (1M context) --- api.php | 60 ++-- .../GravityPdfFontDownloadException.php | 24 ++ src/Helper/Fonts/RemoteFontDownloader.php | 166 +++++++++ .../Fonts/Test_RemoteFontDownloader.php | 324 ++++++++++++++++++ tests/phpunit/integration/Test_Api.php | 136 ++++++++ 5 files changed, 688 insertions(+), 22 deletions(-) create mode 100644 src/Exceptions/GravityPdfFontDownloadException.php create mode 100644 src/Helper/Fonts/RemoteFontDownloader.php create mode 100644 tests/phpunit/integration/Helper/Fonts/Test_RemoteFontDownloader.php diff --git a/api.php b/api.php index 6d0fb2806..4fe71729d 100644 --- a/api.php +++ b/api.php @@ -647,18 +647,21 @@ public static function get_pdf_fonts() { * 'font_name' => 'Lato', * 'regular' => '/full/path/to/font/Lato-Regular.ttf', * 'italics' => '/full/path/to/font/Lato-Italic.ttf', - * 'bold' => '/full/path/to/font/Lato-Bold.ttf', - * 'bolditalics' => '/full/path/to/font/Lato-BoldItalic.ttf', + * 'bold' => 'https://example.com/fonts/Lato-Bold.ttf', + * 'bolditalics' => 'https://example.com/fonts/Lato-BoldItalic.ttf', * ) * * Only the 'font_name' and 'regular' keys are required. - * All fonts should be referenced with the full server path. + * Fonts should be referenced with the full server path, or a http(s) URL that is downloaded + * with wp_remote_get(). URLs must point to a .ttf file, resolve to a public address, and the + * response cannot exceed 32MB. * Currently, only .ttf fonts are supported. * The font name can only contain alphanumeric characters, or a space * * @return bool|WP_Error * * @since 4.1 + * @since 6.17 Added support for http(s) URLs */ public static function add_pdf_font( $font ) { @@ -683,30 +686,43 @@ public static function add_pdf_font( $font ) { $request = new WP_REST_Request(); $request->set_param( 'label', $font['font_name'] ?? '' ); - foreach ( $controller->get_font_keys() as $id ) { - if ( isset( $font[ $id ] ) && is_file( $font[ $id ] ) ) { - /* phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents */ - $_FILES[ $id ] = [ - 'file' => file_get_contents( $font[ $id ] ), - 'name' => basename( $font[ $id ] ), - 'size' => filesize( $font[ $id ] ), - 'tmp_name' => $font[ $id ], - 'error' => UPLOAD_ERR_OK, - ]; + $downloader = new \GFPDF\Helper\Fonts\RemoteFontDownloader(); + + $id = ''; + $path = ''; + + try { + foreach ( $controller->get_font_keys() as $id ) { + $path = $font[ $id ] ?? ''; + if ( ! is_string( $path ) || $path === '' ) { + continue; + } + + /* Abort on the first bad URL rather than downloading fonts this call is going to discard */ + if ( \GFPDF\Helper\Fonts\RemoteFontDownloader::is_url( $path ) ) { + $_FILES[ $id ] = $downloader->download( $path ); + } elseif ( is_file( $path ) ) { + $_FILES[ $id ] = [ + 'name' => basename( $path ), + 'tmp_name' => $path, + 'error' => UPLOAD_ERR_OK, + ]; + } } - } - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- $_FILES populated above from $font arg, not a form post. - $request->set_file_params( $_FILES ); - $response = $controller->add_item( $request ); + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- $_FILES populated above from $font arg, not a form post. + $request->set_file_params( $_FILES ); + $response = $controller->add_item( $request ); - $_FILES = $files_backup; + return is_wp_error( $response ) ? $response : true; + } catch ( \GFPDF\Exceptions\GravityPdfFontDownloadException $e ) { + self::get_log_class()->error( $e->getMessage(), [ 'url' => $path ] ); - if ( is_wp_error( $response ) ) { - return $response; + return new WP_Error( 'font_download_error', [ $id => $e->getMessage() ], [ 'status' => 400 ] ); + } finally { + $_FILES = $files_backup; + $downloader->cleanup(); } - - return true; } /** diff --git a/src/Exceptions/GravityPdfFontDownloadException.php b/src/Exceptions/GravityPdfFontDownloadException.php new file mode 100644 index 000000000..abd2d6706 --- /dev/null +++ b/src/Exceptions/GravityPdfFontDownloadException.php @@ -0,0 +1,24 @@ +get_filename_from_url( $url ); + + if ( ! function_exists( 'wp_tempnam' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $tmp_name = wp_tempnam( $filename ); + $this->temp_files[] = $tmp_name; + + $response = wp_remote_get( + $url, + [ + 'timeout' => 30, + /* Each redirect target is revalidated against reject_unsafe_urls by WP_Http */ + 'redirection' => 3, + 'reject_unsafe_urls' => true, + 'limit_response_size' => static::MAX_FILE_SIZE + 1, + 'stream' => true, + 'filename' => $tmp_name, + ] + ); + + if ( is_wp_error( $response ) ) { + /* translators: %s: the HTTP error message */ + throw new GravityPdfFontDownloadException( sprintf( esc_html__( 'Could not download the font: %s', 'gravity-pdf' ), esc_html( $response->get_error_message() ) ) ); + } + + $status = (int) wp_remote_retrieve_response_code( $response ); + if ( $status !== 200 ) { + /* translators: %d: the HTTP status code returned by the remote server */ + throw new GravityPdfFontDownloadException( sprintf( esc_html__( 'Could not download the font. The server responded with a %d status code.', 'gravity-pdf' ), esc_html( (string) $status ) ) ); + } + + clearstatcache( true, $tmp_name ); + $size = (int) @filesize( $tmp_name ); /* phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged */ + + if ( $size === 0 ) { + throw new GravityPdfFontDownloadException( esc_html__( 'The downloaded font file is empty.', 'gravity-pdf' ) ); + } + + /* The response is capped at MAX_FILE_SIZE + 1 bytes, so anything larger is either oversized or truncated */ + if ( $size > static::MAX_FILE_SIZE ) { + throw new GravityPdfFontDownloadException( + sprintf( + /* translators: %s: the maximum font file size e.g. 32 MB */ + esc_html__( 'The font file exceeds the maximum size of %s.', 'gravity-pdf' ), + esc_html( (string) size_format( static::MAX_FILE_SIZE ) ) + ) + ); + } + + return [ + 'name' => $filename, + 'tmp_name' => $tmp_name, + 'error' => UPLOAD_ERR_OK, + ]; + } + + /** + * Delete any temporary files created by this instance + * + * @since 6.17 + */ + public function cleanup(): void { + foreach ( $this->temp_files as $file ) { + if ( is_file( $file ) ) { + wp_delete_file( $file ); + } + } + + $this->temp_files = []; + } + + /** + * Derive a safe .ttf filename from the URL path + * + * @throws GravityPdfFontDownloadException + * + * @since 6.17 + */ + protected function get_filename_from_url( string $url ): string { + $path = rawurldecode( (string) wp_parse_url( $url, PHP_URL_PATH ) ); + + /* basename() before sanitizing so any traversal segments in the decoded path are discarded */ + $filename = sanitize_file_name( basename( $path ) ); + + if ( strtolower( (string) pathinfo( $filename, PATHINFO_EXTENSION ) ) !== 'ttf' ) { + throw new GravityPdfFontDownloadException( esc_html__( 'The font URL must point to a .ttf file.', 'gravity-pdf' ) ); + } + + return $filename; + } +} diff --git a/tests/phpunit/integration/Helper/Fonts/Test_RemoteFontDownloader.php b/tests/phpunit/integration/Helper/Fonts/Test_RemoteFontDownloader.php new file mode 100644 index 000000000..c855cff64 --- /dev/null +++ b/tests/phpunit/integration/Helper/Fonts/Test_RemoteFontDownloader.php @@ -0,0 +1,324 @@ +downloader = new RemoteFontDownloader(); + $this->request_args = []; + } + + public function tear_down(): void { + $this->downloader->cleanup(); + + parent::tear_down(); + } + + /** + * Short-circuit wp_remote_get() and write $body to the stream target, mimicking a real download + * + * @param callable|string $body String written to the stream target, or a callable given the target path + */ + private function mock_http( $body = '', int $status = 200, ?\WP_Error $error = null ): void { + add_filter( + 'pre_http_request', + function ( $preempt, $args ) use ( $body, $status, $error ) { + $this->request_args = $args; + + if ( $error !== null ) { + return $error; + } + + if ( ! empty( $args['filename'] ) ) { + if ( is_callable( $body ) ) { + $body( $args['filename'] ); + } else { + file_put_contents( $args['filename'], $body ); + } + } + + return [ + 'headers' => [], + 'body' => '', + 'response' => [ + 'code' => $status, + 'message' => get_status_header_desc( $status ), + ], + 'cookies' => [], + 'filename' => $args['filename'] ?? null, + ]; + }, + 10, + 2 + ); + } + + private function chewy(): string { + return (string) file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/Chewy.ttf' ); + } + + /** + * @dataProvider provider_is_url + */ + public function test_is_url( bool $expected, string $value ): void { + $this->assertSame( $expected, RemoteFontDownloader::is_url( $value ) ); + } + + public function provider_is_url(): array { + return [ + 'http' => [ true, 'http://example.com/font.ttf' ], + 'https' => [ true, 'https://example.com/font.ttf' ], + 'uppercase' => [ true, 'HTTPS://example.com/font.ttf' ], + 'ftp' => [ true, 'ftp://example.com/font.ttf' ], + 'file' => [ true, 'file:///etc/passwd' ], + 'php stream' => [ true, 'php://filter/resource=/etc/passwd' ], + 'absolute path' => [ false, '/var/www/fonts/font.ttf' ], + 'windows path' => [ false, 'C:\\fonts\\font.ttf' ], + 'relative path' => [ false, 'fonts/font.ttf' ], + 'protocol-less' => [ false, '//example.com/font.ttf' ], + 'empty' => [ false, '' ], + 'name only' => [ false, 'font.ttf' ], + ]; + } + + /** + * @dataProvider provider_rejected_urls + */ + public function test_download_rejects_url( string $url, string $expected_message ): void { + $this->mock_http(); + + $this->expectException( GravityPdfFontDownloadException::class ); + $this->expectExceptionMessage( $expected_message ); + + $this->downloader->download( $url ); + } + + public function provider_rejected_urls(): array { + return [ + 'ftp scheme' => [ 'ftp://93.184.216.34/font.ttf', 'Only http:// and https:// font URLs are supported.' ], + 'file scheme' => [ 'file:///etc/passwd', 'Only http:// and https:// font URLs are supported.' ], + 'php stream' => [ 'php://filter/resource=/etc/passwd', 'Only http:// and https:// font URLs are supported.' ], + 'no scheme' => [ '/var/www/fonts/font.ttf', 'Only http:// and https:// font URLs are supported.' ], + 'loopback' => [ 'http://127.0.0.1/font.ttf', 'resolves to a restricted address' ], + 'private class a' => [ 'http://10.0.0.5/font.ttf', 'resolves to a restricted address' ], + 'private class b' => [ 'http://172.16.4.1/font.ttf', 'resolves to a restricted address' ], + 'private class c' => [ 'http://192.168.1.1/font.ttf', 'resolves to a restricted address' ], + 'link local' => [ 'http://0.0.0.0/font.ttf', 'resolves to a restricted address' ], + 'blocked port' => [ 'http://93.184.216.34:22/font.ttf', 'resolves to a restricted address' ], + 'credentials' => [ 'http://user:pass@93.184.216.34/font.ttf', 'resolves to a restricted address' ], + 'not a ttf' => [ 'http://93.184.216.34/font.otf', 'The font URL must point to a .ttf file.' ], + 'no extension' => [ 'http://93.184.216.34/font', 'The font URL must point to a .ttf file.' ], + 'no path' => [ 'http://93.184.216.34', 'The font URL must point to a .ttf file.' ], + ]; + } + + /** + * A rejected URL must never reach the HTTP layer + */ + public function test_download_does_not_request_rejected_url(): void { + $this->mock_http(); + + try { + $this->downloader->download( 'http://127.0.0.1/font.ttf' ); + } catch ( GravityPdfFontDownloadException $e ) { + $this->assertSame( [], $this->request_args ); + + return; + } + + $this->fail( 'Expected a GravityPdfFontDownloadException.' ); + } + + public function test_download_returns_file_details(): void { + $font = $this->chewy(); + $this->mock_http( $font ); + + $file = $this->downloader->download( self::REMOTE_URL ); + + $this->assertSame( 'Chewy.ttf', $file['name'] ); + $this->assertSame( UPLOAD_ERR_OK, $file['error'] ); + $this->assertFileExists( $file['tmp_name'] ); + $this->assertSame( $font, file_get_contents( $file['tmp_name'] ) ); + } + + public function test_download_uses_hardened_request_args(): void { + $this->mock_http( $this->chewy() ); + $this->downloader->download( self::REMOTE_URL ); + + $this->assertTrue( $this->request_args['reject_unsafe_urls'] ); + $this->assertTrue( $this->request_args['stream'] ); + $this->assertSame( RemoteFontDownloader::MAX_FILE_SIZE + 1, $this->request_args['limit_response_size'] ); + $this->assertSame( 3, $this->request_args['redirection'] ); + $this->assertSame( 30, $this->request_args['timeout'] ); + $this->assertNotEmpty( $this->request_args['filename'] ); + } + + /** + * The font is streamed to a temp file, not into the custom font directory + */ + public function test_download_streams_to_temp_directory(): void { + $this->mock_http( $this->chewy() ); + + $file = $this->downloader->download( self::REMOTE_URL ); + + $this->assertStringStartsWith( get_temp_dir(), $file['tmp_name'] ); + $this->assertFileDoesNotExist( $this->gfpdf()->data->template_font_location . 'Chewy.ttf' ); + } + + /** + * @dataProvider provider_filenames + */ + public function test_download_derives_filename_from_url( string $expected, string $url ): void { + $this->mock_http( $this->chewy() ); + + $file = $this->downloader->download( $url ); + + $this->assertSame( $expected, $file['name'] ); + } + + public function provider_filenames(): array { + return [ + 'plain' => [ 'Chewy.ttf', 'http://93.184.216.34/Chewy.ttf' ], + 'nested path' => [ 'Chewy.ttf', 'http://93.184.216.34/a/b/c/Chewy.ttf' ], + 'query string' => [ 'Chewy.ttf', 'http://93.184.216.34/Chewy.ttf?v=2&x=1' ], + 'fragment' => [ 'Chewy.ttf', 'http://93.184.216.34/Chewy.ttf#frag' ], + 'encoded space' => [ 'My-Font.ttf', 'http://93.184.216.34/My%20Font.ttf' ], + 'encoded slash' => [ 'Chewy.ttf', 'http://93.184.216.34/fonts%2FChewy.ttf' ], + 'traversal' => [ 'Chewy.ttf', 'http://93.184.216.34/a/../../Chewy.ttf' ], + 'uppercase ext' => [ 'Chewy.TTF', 'http://93.184.216.34/Chewy.TTF' ], + ]; + } + + public function test_download_throws_on_http_error(): void { + $this->mock_http( '', 200, new \WP_Error( 'http_request_failed', 'Connection timed out' ) ); + + $this->expectException( GravityPdfFontDownloadException::class ); + $this->expectExceptionMessage( 'Connection timed out' ); + + $this->downloader->download( self::REMOTE_URL ); + } + + /** + * @dataProvider provider_error_status_codes + */ + public function test_download_throws_on_non_200_response( int $status ): void { + $this->mock_http( $this->chewy(), $status ); + + $this->expectException( GravityPdfFontDownloadException::class ); + $this->expectExceptionMessage( sprintf( 'The server responded with a %d status code.', $status ) ); + + $this->downloader->download( self::REMOTE_URL ); + } + + public function provider_error_status_codes(): array { + return [ [ 301 ], [ 401 ], [ 403 ], [ 404 ], [ 500 ] ]; + } + + public function test_download_throws_on_empty_response(): void { + $this->mock_http( '' ); + + $this->expectException( GravityPdfFontDownloadException::class ); + $this->expectExceptionMessage( 'The downloaded font file is empty.' ); + + $this->downloader->download( self::REMOTE_URL ); + } + + /** + * Write a sparse file of $bytes — the size the transport truncates an oversized response to + */ + private function mock_http_body_of_size( int $bytes ): void { + $this->mock_http( + function ( $path ) use ( $bytes ) { + $fh = fopen( $path, 'w' ); + fseek( $fh, $bytes - 1 ); + fwrite( $fh, 'a' ); + fclose( $fh ); + } + ); + } + + public function test_download_throws_when_response_exceeds_max_size(): void { + $this->mock_http_body_of_size( RemoteFontDownloader::MAX_FILE_SIZE + 1 ); + + $this->expectException( GravityPdfFontDownloadException::class ); + $this->expectExceptionMessage( 'exceeds the maximum size' ); + + $this->downloader->download( self::REMOTE_URL ); + } + + public function test_download_accepts_response_at_max_size(): void { + $this->mock_http_body_of_size( RemoteFontDownloader::MAX_FILE_SIZE ); + + $file = $this->downloader->download( self::REMOTE_URL ); + + $this->assertSame( RemoteFontDownloader::MAX_FILE_SIZE, filesize( $file['tmp_name'] ) ); + } + + public function test_cleanup_deletes_temp_files(): void { + $this->mock_http( $this->chewy() ); + + $first = $this->downloader->download( self::REMOTE_URL ); + $second = $this->downloader->download( self::REMOTE_URL ); + + $this->assertNotSame( $first['tmp_name'], $second['tmp_name'] ); + + $this->downloader->cleanup(); + + $this->assertFileDoesNotExist( $first['tmp_name'] ); + $this->assertFileDoesNotExist( $second['tmp_name'] ); + } + + /** + * A failed download must not leave the placeholder temp file behind + */ + public function test_cleanup_deletes_temp_file_after_failed_download(): void { + $this->mock_http( '', 404 ); + + try { + $this->downloader->download( self::REMOTE_URL ); + $this->fail( 'Expected a GravityPdfFontDownloadException.' ); + } catch ( GravityPdfFontDownloadException $e ) { + $temp_files = glob( get_temp_dir() . 'Chewy-*.tmp' ); + + $this->downloader->cleanup(); + + $this->assertNotEmpty( $temp_files ); + foreach ( $temp_files as $file ) { + $this->assertFileDoesNotExist( $file ); + } + } + } + + public function test_cleanup_is_idempotent(): void { + $this->mock_http( $this->chewy() ); + $file = $this->downloader->download( self::REMOTE_URL ); + + $this->downloader->cleanup(); + $this->downloader->cleanup(); + + $this->assertFileDoesNotExist( $file['tmp_name'] ); + } +} diff --git a/tests/phpunit/integration/Test_Api.php b/tests/phpunit/integration/Test_Api.php index 80a374566..96d626af3 100644 --- a/tests/phpunit/integration/Test_Api.php +++ b/tests/phpunit/integration/Test_Api.php @@ -292,6 +292,142 @@ public function test_add_pdf_font_duplicate() { GPDFAPI::delete_pdf_font( 'test' ); } + /** + * A public IP literal. Using an IP (instead of a hostname) keeps wp_http_validate_url() from + * making a DNS lookup, so these tests never touch the network. + */ + private const FONT_URL = 'http://93.184.216.34/fonts/Chewy.ttf'; + + /** + * Short-circuit wp_remote_get() and write the fixture font to the stream target + */ + private function mock_font_download( string $body, int $status = 200 ): void { + add_filter( + 'pre_http_request', + function ( $preempt, $args ) use ( $body, $status ) { + if ( ! empty( $args['filename'] ) ) { + file_put_contents( $args['filename'], $body ); + } + + return [ + 'headers' => [], + 'body' => '', + 'response' => [ + 'code' => $status, + 'message' => get_status_header_desc( $status ), + ], + 'cookies' => [], + 'filename' => $args['filename'] ?? null, + ]; + }, + 10, + 2 + ); + } + + /** + * @since 6.17 + */ + public function test_add_pdf_font_from_url() { + $this->mock_font_download( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/Chewy.ttf' ) ); + + $results = GPDFAPI::add_pdf_font( + [ + 'font_name' => 'Test', + 'regular' => self::FONT_URL, + ] + ); + + $this->assertTrue( $results ); + $this->assertFileExists( PDF_FONT_LOCATION . 'Chewy.ttf' ); + + /* The temporary download is removed once the font is installed */ + $this->assertSame( [], glob( get_temp_dir() . 'Chewy-*.tmp' ) ); + + GPDFAPI::delete_pdf_font( 'test' ); + } + + /** + * URLs and absolute paths can be mixed within the one font + * + * @since 6.17 + */ + public function test_add_pdf_font_from_url_and_path() { + $this->mock_font_download( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/DejaVuSans-Bold.ttf' ) ); + + $results = GPDFAPI::add_pdf_font( + [ + 'font_name' => 'Test', + 'regular' => PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/Chewy.ttf', + 'bold' => 'http://93.184.216.34/fonts/DejaVuSans-Bold.ttf', + ] + ); + + $this->assertTrue( $results ); + + $font = GPDFAPI::get_mvc_class( 'Model_Custom_Fonts' )->get_font_by_id( 'test' ); + $this->assertSame( PDF_FONT_LOCATION . 'Chewy.ttf', $font['regular'] ); + $this->assertFileExists( $font['bold'] ); + + GPDFAPI::delete_pdf_font( 'test' ); + } + + /** + * A download failure surfaces as a font_download_error keyed by the font slot that failed + * + * The rejection rules themselves are covered by Test_RemoteFontDownloader; this only pins the wiring. + * + * @dataProvider provider_invalid_font_urls + * + * @since 6.17 + */ + public function test_add_pdf_font_url_errors( string $url, int $status ) { + $this->mock_font_download( file_get_contents( PDF_PLUGIN_DIR . '/tools/phpunit/data/fonts/Chewy.ttf' ), $status ); + + $results = GPDFAPI::add_pdf_font( + [ + 'font_name' => 'Test', + 'regular' => $url, + ] + ); + + $this->assertInstanceOf( \WP_Error::class, $results ); + $this->assertSame( 'font_download_error', $results->get_error_code() ); + $this->assertArrayHasKey( 'regular', $results->get_error_message() ); + + /* Nothing is installed, and no temporary file is left behind */ + $this->assertArrayNotHasKey( 'User-Defined Fonts', GPDFAPI::get_pdf_fonts() ); + $this->assertSame( [], glob( get_temp_dir() . 'Chewy-*.tmp' ) ); + } + + public function provider_invalid_font_urls(): array { + return [ + 'rejected before the request' => [ 'http://127.0.0.1/fonts/Chewy.ttf', 200 ], + 'rejected after the request' => [ self::FONT_URL, 404 ], + ]; + } + + /** + * A URL that downloads successfully but isn't a font is still rejected by the TTF validator + * + * @since 6.17 + */ + public function test_add_pdf_font_url_rejects_non_font_content() { + $this->mock_font_download( 'not a font' ); + + $results = GPDFAPI::add_pdf_font( + [ + 'font_name' => 'Test', + 'regular' => self::FONT_URL, + ] + ); + + $this->assertInstanceOf( \WP_Error::class, $results ); + $this->assertSame( 'font_validation_error', $results->get_error_code() ); + $this->assertFileDoesNotExist( PDF_FONT_LOCATION . 'Chewy.ttf' ); + $this->assertSame( [], glob( get_temp_dir() . 'Chewy-*.tmp' ) ); + } + /** * Test we can correctly delete the font *