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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Gravity PDF

### 6.17.0
* 🎉 Feature: Install multiple PDF templates at once by selecting or dropping several zip files together
* 🎉 Feature: Drop PDF template zip files anywhere in the Template Manager window to install them
* 🎉 Feature: Accept PDF template zips with the templates nested inside a folder, which is what you get when re-zipping a Safari download
* 🔒 Security: Harden PDF template and custom font uploads by upgrading the bundled upload library to 4.0
* 🧹 Housekeeping: Raise the PDF template upload limit from 10MB to 32MB, capped by the server's own upload limit
* 💻 Developer: Add `gfpdf_template_max_upload_size` filter to change the maximum PDF template zip size

### 6.16.0
* 🎉 Feature: Gravity Forms 3.0 compatibility
* 🎉 Feature: Support the Gravity Forms 3.0 International (formatted) phone number format, displayed with the country and dial code in front of the number
Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
"url": "https://github.com/GravityPDF/url-signer"
},

{
"type": "vcs",
"url": "https://github.com/GravityPDF/Upload"
},

{
"type": "composer",
"url": "https://composer.gravity.io",
Expand All @@ -38,7 +43,7 @@
"spatie/url-signer": "^1.1",
"mpdf/qrcode": "^1.0",
"gravitypdf/querypath": "^4.0",
"gravitypdf/upload": "^3.0"
"gravitypdf/upload": "dev-hardening"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
Expand Down
36 changes: 28 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/Helper/Fonts/LocalFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ class LocalFile extends File {
* @since 6.4
*/
public function isValid(): bool {
/* Reset rather than append, matching the parent: upload() calls isValid() again, so the
`if ( ! $file->isValid() ) {…} $file->upload();` pattern would otherwise double every error */
$this->errors = $this->constructorErrors;

foreach ( $this->objects as $fileInfo ) {
$this->applyCallback( 'beforeValidationCallback', $fileInfo );
foreach ( $this->validations as $validation ) {
Expand Down
7 changes: 6 additions & 1 deletion src/Helper/Helper_Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ public function addon_license_responses( $addon_name ) {
public function get_localised_script_data( Helper_Abstract_Options $options, Helper_Abstract_Form $gform ) {

$custom_fonts = array_values( $options->get_custom_fonts() );
$max_upload_size = Helper_Templates::get_max_upload_size();
$user_data = get_userdata( get_current_user_id() );
$user_capabilities = is_object( $user_data ) ? $user_data->allcaps : [];
$user_capabilities = is_super_admin() ? [ 'administrator' => true ] : $user_capabilities;
Expand Down Expand Up @@ -282,7 +283,11 @@ public function get_localised_script_data( Helper_Abstract_Options $options, Hel
'showPreviousTemplate' => esc_html__( 'Show previous template', 'gravity-pdf' ),
'showNextTemplate' => esc_html__( 'Show next template', 'gravity-pdf' ),
'uploadInvalidNotZipFile' => esc_html__( 'Upload is not a valid template. Upload a .zip file.', 'gravity-pdf' ),
'uploadInvalidExceedsFileSizeLimit' => esc_html__( 'Upload exceeds the 10MB limit.', 'gravity-pdf' ),
/* translators: %s: the maximum upload size e.g. "32 MB" */
'uploadInvalidExceedsFileSizeLimit' => sprintf( esc_html__( 'Upload exceeds the %s limit.', 'gravity-pdf' ), size_format( $max_upload_size ) ),
'templateUploadMaxSize' => $max_upload_size,
'templateUploadDropzone' => esc_html__( 'Drop PDF template .zip files anywhere to install', 'gravity-pdf' ),
'templateUploadInProgress' => esc_html__( 'Installing PDF template(s)...', 'gravity-pdf' ),
'templateSuccessfullyInstalled' => esc_html__( 'Template successfully installed', 'gravity-pdf' ),
'templateSuccessfullyUpdated' => esc_html__( 'Template successfully updated', 'gravity-pdf' ),
'templateSuccessfullyInstalledUpdated' => esc_html__( 'PDF Template(s) Successfully Installed / Updated', 'gravity-pdf' ),
Expand Down
99 changes: 91 additions & 8 deletions src/Helper/Helper_Templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
*/
class Helper_Templates {

/**
* The largest PDF template zip we'll accept, in bytes
*
* @since 6.17
*/
public const MAX_UPLOAD_SIZE = 32 * MB_IN_BYTES;

/**
* How many nested wrapper directories we'll look through to find the PDF templates
*
* @since 6.17
*/
private const MAX_NESTED_DIRECTORIES = 3;

/**
* Holds our log class
*
Expand Down Expand Up @@ -492,23 +506,92 @@ public function get_core_pdf_templates() {
* @return array
*/
public function get_all_templates_in_folder( $folder ) {
try {
$dir = new \FilesystemIterator( $folder );
$files = new \CallbackFilterIterator(
return $this->list_folder(
$folder,
function ( $current ) {
return ! $current->isDir() && $current->getExtension() === 'php';
}
);
}

/**
* The maximum size, in bytes, of a PDF template zip we'll accept
*
* Clamped to the server's own upload ceiling. A POST larger than `post_max_size` is discarded by PHP
* before the request reaches us, which surfaces as a nonce failure instead of a size error.
*
* @return int
*
* @since 6.17
*/
public static function get_max_upload_size() {
$server_limit = (int) wp_max_upload_size();
$max_size = $server_limit > 0 ? min( static::MAX_UPLOAD_SIZE, $server_limit ) : static::MAX_UPLOAD_SIZE;

return (int) apply_filters( 'gfpdf_template_max_upload_size', $max_size );
}

/**
* Find the directory holding the PDF templates inside an extracted zip
*
* Safari auto-extracts template zips on download. When the resulting folder is re-zipped the PHP files
* sit one (or more) levels deep, so we descend through single-directory wrappers until we find them.
*
* @param string $dir The directory the zip was extracted to
*
* @return string The directory containing the PDF templates, with a trailing slash
*
* @since 6.17
*/
public function get_template_root_dir( $dir ) {
$dir = trailingslashit( $dir );

for ( $depth = 0; $depth < static::MAX_NESTED_DIRECTORIES; $depth++ ) {
if ( count( $this->get_all_templates_in_folder( $dir ) ) > 0 ) {
return $dir;
}

/* Hidden directories are tooling leftovers (.git, .idea), not the template we're looking for */
$subdirectories = $this->list_folder(
$dir,
function ( $current ) {
return ! $current->isDir() && $current->getExtension() === 'php';
return $current->isDir() && $current->getFilename()[0] !== '.';
}
);

$templates = [];
foreach ( $files as $file ) {
$templates[] = $file->getPathname();
/* Anything other than a single wrapper directory is ambiguous, so leave the path alone */
if ( count( $subdirectories ) !== 1 ) {
return $dir;
}

$dir = trailingslashit( $subdirectories[0] );
}

return $dir;
}

/**
* Get the full paths of everything in a folder that matches a filter
*
* @param string $folder
* @param callable $filter Receives a SplFileInfo and returns whether to include it
*
* @return string[]
*
* @since 6.17
*/
protected function list_folder( $folder, callable $filter ) {
try {
$matches = [];

foreach ( new \CallbackFilterIterator( new \FilesystemIterator( $folder ), $filter ) as $item ) {
$matches[] = $item->getPathname();
}

return $templates;
return $matches;
} catch ( \Exception $e ) {
$this->log->error( $e->getMessage() );

return [];
}
}
Expand Down
33 changes: 18 additions & 15 deletions src/Model/Model_Templates.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use GFPDF_Vendor\GravityPdf\Upload\File;
use GFPDF_Vendor\GravityPdf\Upload\Storage\FileSystem;
use GFPDF_Vendor\GravityPdf\Upload\Validation\Extension;
use GFPDF_Vendor\GravityPdf\Upload\Validation\Mimetype;
use GFPDF_Vendor\GravityPdf\Upload\Validation\FileType;
use GFPDF_Vendor\GravityPdf\Upload\Validation\Size;
use GPDFAPI;
use GFPDF_Vendor\Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -124,7 +124,7 @@ public function ajax_process_uploaded_template() {

/* Unzip and check the PDF templates look valid */
try {
$this->unzip_and_verify_templates( $zip_path );
$unzipped_dir_name = $this->unzip_and_verify_templates( $zip_path );
} catch ( Exception $e ) {
$this->cleanup_template_files( $zip_path );

Expand All @@ -149,8 +149,7 @@ public function ajax_process_uploaded_template() {
}

/* Copy all the files to the active PDF working directory */
$unzipped_dir_name = $this->get_unzipped_dir_name( $zip_path );
$template_path = $this->templates->get_template_path();
$template_path = $this->templates->get_template_path();

$results = $this->misc->copyr( $unzipped_dir_name, $template_path );

Expand Down Expand Up @@ -303,22 +302,19 @@ public function move_template_to_tmp_dir( File $file ) {
/* Validate our uploaded file and move to the PDF tmp directory for further processing */
$file->setName( uniqid() );

/* FileType requires the extension and the sniffed contents to describe the same format, where
Extension and Mimetype side by side check two independent allow-lists. It sniffs, so it needs
fileinfo — that should be loaded by default, but where it isn't, Extension on its own beats
refusing every upload. */
$file->addValidations(
[
new Extension( 'zip' ),
new Size( '10240K' ), /* allow 10MB upload – accounts for fonts, PDF and PHP files */
new Size( Helper_Templates::get_max_upload_size() ),
extension_loaded( 'fileinfo' )
? new FileType( 'zip', [ 'application/zip', 'application/octet-stream' ] )
: new Extension( 'zip' ),
]
);

/* Do a check to ensure fileinfo is loaded. It should be loaded by default but in some cases this isn't so */
if ( extension_loaded( 'fileinfo' ) ) {
$file->addValidations(
[
new Mimetype( [ 'application/zip', 'application/octet-stream' ] ),
]
);
}

$file->upload();

return $this->data->template_tmp_location . $file->getNameWithExtension();
Expand All @@ -342,9 +338,12 @@ public function get_unzipped_dir_name( $zip_path ) {
*
* @param string $zip_path The full path to the zip file
*
* @return string The directory the PDF templates were found in
*
* @throws Exception Thrown if a PDF template file isn't valid
*
* @since 4.1
* @since 6.17 Returns the directory the PDF templates were found in
*/
public function unzip_and_verify_templates( $zip_path ) {
$this->enable_wp_filesystem();
Expand All @@ -357,6 +356,8 @@ public function unzip_and_verify_templates( $zip_path ) {
throw new Exception( esc_html( $results->get_error_message() ) );
}

$dir = $this->templates->get_template_root_dir( $dir );

/* Check unzipped templates for a valid v4 header, or v3 string pattern.
Avoid glob() here — it can return a stale (empty) listing when called
immediately after unzip_file() writes via the WP_Filesystem abstraction */
Expand All @@ -367,6 +368,8 @@ public function unzip_and_verify_templates( $zip_path ) {
}

$this->check_for_valid_pdf_templates( $files );

return $dir;
}

/**
Expand Down
Loading
Loading