Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 additions & 22 deletions api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {

Expand All @@ -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;
}

/**
Expand Down
24 changes: 24 additions & 0 deletions src/Exceptions/GravityPdfFontDownloadException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace GFPDF\Exceptions;

/**
* @package Gravity PDF
* @copyright Copyright (c) 2026, Blue Liquid Designs
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
*/

/* Exit if accessed directly */
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class GravityPdfFontDownloadException
*
* @package GFPDF\Exceptions
*
* @since 6.17
*/
class GravityPdfFontDownloadException extends GravityPdfRuntimeException {
}
166 changes: 166 additions & 0 deletions src/Helper/Fonts/RemoteFontDownloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare( strict_types=1 );

namespace GFPDF\Helper\Fonts;

use GFPDF\Exceptions\GravityPdfFontDownloadException;

/**
* @package Gravity PDF
* @copyright Copyright (c) 2026, Blue Liquid Designs
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
*/

/* Exit if accessed directly */
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Downloads a remote TTF font to a temporary file on disk so it can be handed to the Custom Fonts controller
*
* The caller owns the temporary files and must call self::cleanup() once finished with them.
*
* @since 6.17
*/
class RemoteFontDownloader {

/**
* The maximum size, in bytes, a remote font file is allowed to be. Generous enough for the largest CJK fonts.
*
* @since 6.17
*/
public const MAX_FILE_SIZE = 33554432; /* 32MB */

/**
* @var string[] The temporary files created by this instance
* @since 6.17
*/
protected $temp_files = [];

/**
* Whether the value looks like a URL (as opposed to a path on the local filesystem)
*
* Any value with a scheme is treated as a URL so unsupported schemes (ftp://, file://, php://)
* are rejected by self::download() instead of falling through to the local filesystem handling.
*
* @since 6.17
*/
public static function is_url( string $value ): bool {
return (bool) preg_match( '#^[a-z][a-z0-9+.\-]*://#i', $value );
}

/**
* Download a remote TTF font to a temporary file
*
* @return array A synthetic $_FILES entry for the downloaded font
*
* @throws GravityPdfFontDownloadException
*
* @since 6.17
*/
public function download( string $url ): array {
$scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
throw new GravityPdfFontDownloadException( esc_html__( 'Only http:// and https:// font URLs are supported.', 'gravity-pdf' ) );
}

/* Blocks credentials in the URL, non-standard ports, and hosts that resolve to a private/loopback IP */
if ( wp_http_validate_url( $url ) === false ) {
throw new GravityPdfFontDownloadException( esc_html__( 'The font URL is not valid, or resolves to a restricted address.', 'gravity-pdf' ) );
}

$filename = $this->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;
}
}
Loading
Loading