HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/lcc.kaunokolegija.lt/wp-content/plugins/gravity-forms-pdf-extended/src/Model/Model_PDF.php
<?php

namespace GFPDF\Model;

use Exception;
use GF_Field;
use GFCommon;
use GFFormsModel;
use GFPDF\Helper\Fields\Field_Default;
use GFPDF\Helper\Fields\Field_Products;
use GFPDF\Helper\Helper_Abstract_Field_Products;
use GFPDF\Helper\Helper_Abstract_Fields;
use GFPDF\Helper\Helper_Abstract_Form;
use GFPDF\Helper\Helper_Abstract_Model;
use GFPDF\Helper\Helper_Abstract_Options;
use GFPDF\Helper\Helper_Data;
use GFPDF\Helper\Helper_Form;
use GFPDF\Helper\Helper_Interface_Field_Pdf_Config;
use GFPDF\Helper\Helper_Interface_Url_Signer;
use GFPDF\Helper\Helper_Misc;
use GFPDF\Helper\Helper_Notices;
use GFPDF\Helper\Helper_Options_Fields;
use GFPDF\Helper\Helper_PDF;
use GFPDF\Helper\Helper_Templates;
use GFPDF_Vendor\Mpdf\Mpdf;
use GFPDF_Vendor\Spatie\UrlSigner\Exceptions\InvalidSignatureKey;
use GFQuiz;
use GFResults;
use GP_Populate_Anything_Live_Merge_Tags;
use Psr\Log\LoggerInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use WP_Error;

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

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

/**
 * Model_PDF
 *
 * Handles all the PDF display logic
 *
 * @since 4.0
 */
class Model_PDF extends Helper_Abstract_Model {

	/**
	 * Holds the abstracted Gravity Forms API specific to Gravity PDF
	 *
	 * @var Helper_Form
	 *
	 * @since 4.0
	 */
	protected $gform;

	/**
	 * Holds our log class
	 *
	 * @var LoggerInterface
	 *
	 * @since 4.0
	 */
	protected $log;

	/**
	 * Holds our Helper_Abstract_Options / Helper_Options_Fields object
	 * Makes it easy to access global PDF settings and individual form PDF settings
	 *
	 * @var Helper_Options_Fields
	 *
	 * @since 4.0
	 */
	protected $options;

	/**
	 * Holds our Helper_Data object
	 * which we can autoload with any data needed
	 *
	 * @var Helper_Data
	 *
	 * @since 4.0
	 */
	protected $data;

	/**
	 * Holds our Helper_Misc object
	 * Makes it easy to access common methods throughout the plugin
	 *
	 * @var Helper_Misc
	 *
	 * @since 4.0
	 */
	protected $misc;

	/**
	 * Holds our Helper_Notices object
	 * which we can use to queue up admin messages for the user
	 *
	 * @var Helper_Notices
	 *
	 * @since 4.0
	 */
	protected $notices;

	/**
	 * Holds our Helper_Templates object
	 * used to ease access to our PDF templates
	 *
	 * @var Helper_Templates
	 *
	 * @since 4.0
	 */
	protected $templates;

	/**
	 * @var Helper_Interface_Url_Signer
	 *
	 * @since 5.2
	 */
	protected $url_signer;

	/**
	 * Setup our view with the needed data and classes
	 *
	 * @param Helper_Abstract_Form        $gform   Our abstracted Gravity Forms helper functions
	 * @param LoggerInterface             $log     Our logger class
	 * @param Helper_Abstract_Options     $options Our options class which allows us to access any settings
	 * @param Helper_Data                 $data    Our plugin data store
	 * @param Helper_Misc                 $misc    Our miscellaneous class
	 * @param Helper_Notices              $notices Our notice class used to queue admin messages and errors
	 * @param Helper_Templates            $templates
	 * @param Helper_Interface_Url_Signer $url_signer
	 *
	 * @since 4.0
	 */
	public function __construct( Helper_Abstract_Form $gform, LoggerInterface $log, Helper_Abstract_Options $options, Helper_Data $data, Helper_Misc $misc, Helper_Notices $notices, Helper_Templates $templates, Helper_Interface_Url_Signer $url_signer ) {

		/* Assign our internal variables */
		$this->gform      = $gform;
		$this->log        = $log;
		$this->options    = $options;
		$this->data       = $data;
		$this->misc       = $misc;
		$this->notices    = $notices;
		$this->templates  = $templates;
		$this->url_signer = $url_signer;
	}

	/**
	 * Our Middleware used to handle the authentication process
	 *
	 * @param string  $pid    The Gravity Form PDF Settings ID
	 * @param integer $lid    The Gravity Form Entry ID
	 * @param string  $action Whether the PDF should be viewed or downloaded
	 *
	 * @return WP_Error
	 * @since 4.0
	 *
	 */
	public function process_pdf( $pid, $lid, $action = 'view' ) {

		/**
		 * Check if we have a valid Gravity Form Entry and PDF Settings ID
		 */
		$entry = $this->gform->get_entry( $lid );

		/* not a valid entry */
		if ( is_wp_error( $entry ) ) {
			$this->log->error(
				'Invalid Entry.',
				[
					'entry' => $entry,
				]
			);

			return $entry; /* return error */
		}

		$settings = $this->options->get_pdf( $entry['form_id'], $pid );

		/* Not valid settings */
		if ( is_wp_error( $settings ) ) {

			$this->log->error(
				'Invalid PDF Settings.',
				[
					'entry'            => $entry,
					'WP_Error_Message' => $settings->get_error_message(),
					'WP_Error_Code'    => $settings->get_error_code(),
				]
			);

			return $settings; /* return error */
		}

		/* Add our download setting */
		$settings['pdf_action'] = $action;

		/**
		 * Our middleware authenticator
		 * Allow users to tap into our middleware and add or remove additional authentication layers
		 *
		 * Default middleware includes 'middle_public_access', 'middle_active', 'middle_conditional', 'middle_owner_restriction', 'middle_logged_out_timeout', 'middle_auth_logged_out_user', 'middle_user_capability'
		 * If WP_Error is returned the PDF won't be parsed
		 *
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_pdf_middleware/ for more details about this filter
		 */
		$middleware = apply_filters( 'gfpdf_pdf_middleware', false, $entry, $settings );

		/* Throw error */
		if ( is_wp_error( $middleware ) ) {

			$this->log->error(
				'PDF Authentication Failure.',
				[
					'entry'            => $entry,
					'settings'         => $settings,
					'WP_Error_Message' => $middleware->get_error_message(),
					'WP_Error_Code'    => $middleware->get_error_code(),
				]
			);

			return $middleware;
		}

		/* Add backwards compatibility support for certain settings */
		$settings = $this->apply_backwards_compatibility_filters( $settings, $entry );

		/* Ensure Gravity Forms dependency loaded */
		$this->misc->maybe_load_gf_entry_detail_class();

		/* If we are here we can generate our PDF */
		$controller = $this->getController();
		$controller->view->generate_pdf( $entry, $settings );

		return null;
	}

	/**
	 * Apply filters to particular settings to maintain backwards compatibility
	 * Note: If you want to modify the $settings array you should use the new "gfpdf_pdf_config" filter instead
	 *
	 * @param array $settings The PDF settings array
	 * @param array $entry
	 *
	 * @return array           The $settings array
	 *
	 * @since  4.0
	 */
	public function apply_backwards_compatibility_filters( $settings, $entry ) {

		$form = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );

		$settings['filename'] = $this->misc->remove_extension_from_string( apply_filters( 'gfpdfe_pdf_name', $settings['filename'], $form, $entry ) );
		$settings['template'] = $this->misc->remove_extension_from_string( apply_filters( 'gfpdfe_template', $settings['template'], $form, $entry ), '.php' );

		if ( isset( $settings['orientation'] ) ) {
			$settings['orientation'] = apply_filters( 'gfpdf_orientation', $settings['orientation'], $form, $entry );
		}

		if ( isset( $settings['security'] ) ) {
			$settings['security'] = $this->misc->update_deprecated_config( apply_filters( 'gfpdf_security', $settings['security'], $form, $entry ) );
		}

		if ( isset( $settings['privileges'] ) ) {
			$settings['privileges'] = apply_filters( 'gfpdf_privilages', $settings['privileges'], $form, $entry );
		}

		if ( isset( $settings['password'] ) ) {
			$settings['password'] = apply_filters( 'gfpdf_password', $settings['password'], $form, $entry );
		}

		if ( isset( $settings['master_password'] ) ) {
			$settings['master_password'] = apply_filters( 'gfpdf_master_password', $settings['master_password'], $form, $entry );
		}

		if ( isset( $settings['rtl'] ) ) {
			$settings['rtl'] = $this->misc->update_deprecated_config( apply_filters( 'gfpdf_rtl', $settings['rtl'], $form, $entry ) );
		}

		return $settings;
	}

	/**
	 * Check if the current PDF trying to be viewed has public access enabled
	 * If it does, we'll remove some of our middleware filters to allow this feature
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @since 4.0
	 */
	public function middle_public_access( $action, $entry, $settings ) {

		if ( isset( $settings['public_access'] ) && 'Yes' === $settings['public_access'] ) {
			remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_owner_restriction' ], 40 );
			remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_logged_out_timeout' ], 50 );
			remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_auth_logged_out_user' ], 60 );
			remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_user_capability' ], 70 );

			$this->log->notice(
				'Public access enabled for current PDF',
				[
					'entry_id' => $entry['id'],
					'pdf_id'   => $settings['id'],
				]
			);
		}

		return $action;
	}

	/**
	 * Check if a signed URL exists and validate. If it passes, disable the remaining middleware capabilities
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @since 5.1
	 */
	public function middle_signed_url_access( $action, $entry, $settings ) {

		/* phpcs:ignore WordPress.Security.NonceVerification.Recommended */
		if ( isset( $_GET['expires'] ) && isset( $_GET['signature'] ) && isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
			try {
				$protocol = isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
				$domain   = $_SERVER['HTTP_HOST'];
				$request  = $_SERVER['REQUEST_URI'];

				$url = esc_url_raw( $protocol . $domain . $request );

				if ( $this->url_signer->verify( $url ) ) {
					remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_owner_restriction' ], 40 );
					remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_logged_out_timeout' ], 50 );
					remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_auth_logged_out_user' ], 60 );
					remove_filter( 'gfpdf_pdf_middleware', [ $this, 'middle_user_capability' ], 70 );

					$this->log->notice(
						'Valid PDF Signing Request',
						[
							'entry_id'    => $entry['id'],
							'pdf_id'      => $settings['id'],
							'url'         => $url,
							'protocol'    => $protocol, /* Logged to a plain text file */
							'domain'      => $domain, /* Logged to a plain text file */
							'request_uri' => $request, /* Logged to a plain text file */
						]
					);
				} else {
					$this->log->warning(
						'Invalid PDF Signing Request',
						[
							'entry_id'    => $entry['id'],
							'pdf_id'      => $settings['id'],
							'url'         => $url,
							'protocol'    => $protocol, /* Logged to a plain text file */
							'domain'      => $domain, /* Logged to a plain text file */
							'request_uri' => $request, /* Logged to a plain text file */
						]
					);
				}
			} catch ( InvalidSignatureKey $e ) {

			}
		}

		return $action;
	}

	/**
	 * Check if the current PDF trying to be viewed is active
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @since 4.0
	 */
	public function middle_active( $action, $entry, $settings ) {

		if ( ! is_wp_error( $action ) ) {
			if ( $settings['active'] !== true ) {
				return new WP_Error( 'inactive', esc_html__( 'The PDF configuration is not currently active.', 'gravity-forms-pdf-extended' ) );
			}
		}

		return $action;
	}

	/**
	 * Check if the current PDF trying to be viewed has conditional logic which passes
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @since 4.0
	 */
	public function middle_conditional( $action, $entry, $settings ) {

		if ( ! is_wp_error( $action ) ) {
			if ( isset( $settings['conditionalLogic'] ) && ! $this->misc->evaluate_conditional_logic( $settings['conditionalLogic'], $entry ) ) {
				return new WP_Error( 'conditional_logic', esc_html__( 'PDF conditional logic requirements have not been met.', 'gravity-forms-pdf-extended' ) );
			}
		}

		return $action;
	}

	/**
	 * If the owner is restricted and the user is not logged in, prompt to log in
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function middle_owner_restriction( $action, $entry, $settings ) {

		/* ensure another middleware filter hasn't already done validation */
		if ( ! is_wp_error( $action ) ) {
			/* get the setting */
			$owner_restriction = $settings['restrict_owner'] ?? 'No';

			if ( $owner_restriction === 'Yes' && ! is_user_logged_in() ) {

				$this->log->notice(
					'Restrict Owner Global Setting Enabled. Prompting logged-out user to login.',
					[
						'entry_id'    => $entry['id'],
						'settings_id' => $settings['id'],
					]
				);

				/* prompt user to login */
				auth_redirect();
			}
		}

		return $action;
	}

	/**
	 * Check the "Logged Out Timeout" global setting and validate it against the current user
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function middle_logged_out_timeout( $action, $entry, $settings ) {

		/* ensure another middleware filter hasn't already done validation */
		if ( ! is_wp_error( $action ) ) {

			/* only check if PDF timed out if our logged out restriction is not 'Yes' and the user is not logged in */
			if ( ! is_user_logged_in() && $this->is_current_pdf_owner( $entry, 'logged_out' ) === true ) {
				/* get the global PDF settings */
				$timeout = (int) $this->options->get_option( 'logged_out_timeout', '20' );

				/* if '0' there is no timeout, or if the logged out restrictions are enabled we'll ignore this */
				if ( $timeout !== 0 ) {

					$timeout_stamp   = 60 * $timeout; /* 60 seconds multiplied by number of minutes */
					$entry_created   = strtotime( $entry['date_created'] ); /* get entry timestamp */
					$timeout_expires = $entry_created + $timeout_stamp; /* get the timeout expiry based on the entry created time */
					$current_time    = time();

					/* compare our two timestamps and throw error if outside the timeout */
					if ( $current_time > $timeout_expires ) {

						/* if there is no user account assigned to this entry throw error */
						if ( empty( $entry['created_by'] ) ) {
							$this->log->notice(
								'Logged Out Timeout Expired. Showing Error Message.',
								[
									'entry_id'        => $entry['id'],
									'settings_id'     => $settings['id'],
									'current_time'    => $current_time,
									'timeout_expires' => $timeout_expires,
								]
							);

							return new WP_Error( 'timeout_expired', esc_html__( 'Your PDF is no longer accessible.', 'gravity-forms-pdf-extended' ) );
						} else {

							$this->log->notice(
								'Logged Out Timeout Expired but user assigned to the entry. Redirecting to Login.',
								[
									'entry_id'        => $entry['id'],
									'settings_id'     => $settings['id'],
									'current_time'    => $current_time,
									'timeout_expires' => $timeout_expires,
								]
							);

							/* prompt to login */
							auth_redirect();
						}
					}
				}
			}
		}

		return $action;
	}

	/**
	 * Check if the current user attempting to access is the PDF owner
	 *
	 * @param array  $entry The Gravity Forms Entry
	 * @param string $type  The authentication type we should use
	 *
	 * @return boolean
	 *
	 * @since 4.0
	 */
	public function is_current_pdf_owner( $entry, $type = 'all' ) {
		$owner = false;
		/* check if the user is logged in and the entry is assigned to them */
		if ( $type === 'all' || $type === 'logged_in' ) {
			if ( is_user_logged_in() && (int) $entry['created_by'] === get_current_user_id() ) {
				$owner = true;
				$this->log->notice(
					'Current logged-in user is the owner of the entry',
					[
						'entry_id'   => $entry['id'],
						'created_by' => $entry['created_by'],
					]
				);
			}
		}

		if ( $type === 'all' || $type === 'logged_out' ) {
			$user_ip   = filter_var( GFFormsModel::get_ip(), FILTER_VALIDATE_IP );
			$server_ip = filter_var( $_SERVER['SERVER_ADDR'] ?? '127.0.0.1', FILTER_VALIDATE_IP );
			$entry_ip  = filter_var( $entry['ip'], FILTER_VALIDATE_IP );

			/* check if the user IP matches the entry IP */
			if (
				! empty( $entry_ip ) &&
				$entry_ip === $user_ip &&
				$entry_ip !== $server_ip
			) {

				$owner = true;
				$this->log->notice(
					'Current logged-out user matches the entry IP address',
					[
						'entry_id'  => $entry['id'],
						'user_ip'   => $user_ip,
						'server_ip' => $server_ip,
					]
				);
			}
		}

		return $owner;
	}

	/**
	 * Check if the user is logged out and authenticate as needed
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function middle_auth_logged_out_user( $action, $entry, $settings ) {

		if ( ! is_wp_error( $action ) ) {

			/* check if the user is not the current entry owner */
			if ( ! is_user_logged_in() && $this->is_current_pdf_owner( $entry, 'logged_out' ) === false ) {
				/* check if there is actually a user who owns entry */
				if ( ! empty( $entry['created_by'] ) ) {

					$this->log->notice(
						'The logged out security checks failed, but there is a logged-in user assigned to the entry. Prompting user to login.',
						[
							'entry_id'    => $entry['id'],
							'settings_id' => $settings['id'],
						]
					);

					/* prompt user to login to get access */
					auth_redirect();
				} else {
					$this->log->warning(
						'The logged out security checks failed and there is no logged-in user assigned to the entry.',
						[
							'entry_id'    => $entry['id'],
							'settings_id' => $settings['id'],
						]
					);

					/* there's no returning, throw generic error */

					return new WP_Error( 'access_denied', esc_html__( 'You do not have access to view this PDF.', 'gravity-forms-pdf-extended' ) );
				}
			}
		}

		return $action;
	}

	/**
	 * Verify the logged-in user can view the PDF
	 *
	 * If owner restrictions are enabled, check if the user as correct capability to view
	 * If owner restrictions are disabled, check if the user is the entry owner
	 *
	 * @param boolean|object $action
	 * @param array          $entry    The Gravity Forms Entry
	 * @param array          $settings The Gravity Form PDF Settings
	 *
	 * @return boolean|object
	 *
	 * @since 4.0
	 */
	public function middle_user_capability( $action, $entry, $settings ) {

		if ( ! is_wp_error( $action ) ) {
			/* check if the user is logged in but is not the current owner */
			$owner_restriction = $settings['restrict_owner'] ?? 'No';

			if (
				is_user_logged_in() &&
				! $this->can_user_view_pdf_with_capabilities() &&
				(
					$owner_restriction === 'Yes' ||
					$this->is_current_pdf_owner( $entry, 'logged_in' ) === false
				)
			) {
				return new WP_Error( 'access_denied', esc_html__( 'You do not have access to view this PDF.', 'gravity-forms-pdf-extended' ) );
			}
		}

		return $action;
	}

	/**
	 * Check if the logged in user has permission to view the PDF
	 *
	 * @param int|null $user_id
	 *
	 * @return bool
	 *
	 * @since 6.8
	 */
	public function can_user_view_pdf_with_capabilities( $user_id = null ) {
		$admin_permissions = $this->options->get_option( 'admin_capabilities', [ 'gravityforms_view_entries' ] );

		/* loop through permissions and check if the current user has any of those capabilities */
		$can_user_view_pdf = false;
		foreach ( $admin_permissions as $permission ) {
			if ( $this->gform->has_capability( $permission, $user_id ) ) {
				$can_user_view_pdf = true;
				break;
			}
		}

		return $can_user_view_pdf;
	}

	/**
	 * Display PDF on Gravity Form entry list page
	 *
	 * @param integer $form_id  Gravity Form ID
	 * @param integer $field_id Current field ID
	 * @param mixed   $value    Current value of field
	 * @param array   $entry    Entry Information
	 *
	 * @return void
	 *
	 * @since 4.0
	 */
	public function view_pdf_entry_list( $form_id, $field_id, $value, $entry ) {

		/* Only show the PDF metabox if a user has permission to view the documents */
		if ( ! $this->can_user_view_pdf_with_capabilities() ) {
			return;
		}

		$controller = $this->getController();
		$pdf_list   = $this->get_pdf_display_list( $entry );

		if ( empty( $pdf_list ) ) {
			return;
		}

		if ( count( $pdf_list ) > 1 ) {
			$args = [
				'pdfs' => $pdf_list,
				'view' => strtolower( $this->options->get_option( 'default_action' ) ),
			];

			$controller->view->entry_list_pdf_multiple( $args );
		} else {
			/* Only one PDF for this form so display a simple 'View PDF' link */
			$args = [
				'pdf'  => array_shift( $pdf_list ),
				'view' => strtolower( $this->options->get_option( 'default_action' ) ),
			];

			$controller->view->entry_list_pdf_single( $args );
		}
	}

	/**
	 * Get a preformatted list of active PDFs with name and URL
	 *
	 * @param array $entry
	 *
	 * @return array
	 *
	 * @since 4.0
	 */
	public function get_pdf_display_list( $entry ) {

		/* Stores our formatted PDFs */
		$args = [];

		/* Check if we have any PDFs */
		$form = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );
		$pdfs = ( isset( $form['gfpdf_form_settings'] ) ) ? $this->get_active_pdfs( $form['gfpdf_form_settings'], $entry ) : [];

		if ( ! empty( $pdfs ) ) {

			foreach ( $pdfs as $settings ) {

				$args[] = [
					'name'     => $this->get_pdf_name( $settings, $entry ),
					'view'     => $this->get_pdf_url( $settings['id'], $entry['id'], false ),
					'download' => $this->get_pdf_url( $settings['id'], $entry['id'], true ),
					'settings' => $settings,
					'entry_id' => $entry['id'],
					'form_id'  => $form['id'],
					'class'    => 'gravitypdf-download-link',
				];
			}
		}

		/**
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_get_pdf_display_list/ for usage
		 *
		 * @since 4.2
		 */
		return apply_filters( 'gfpdf_get_pdf_display_list', $args, $entry, $form );
	}

	/**
	 * Filter out inactive PDFs and those who don't meet the conditional logic
	 *
	 * @param array $pdfs  The PDF settings array
	 * @param array $entry The current entry information
	 *
	 * @return array       The filtered PDFs
	 *
	 * @since 4.0
	 */
	public function get_active_pdfs( $pdfs, $entry ) {

		$filtered = [];
		$form     = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );

		foreach ( $pdfs as $pdf ) {
			if ( $pdf['active'] && ( empty( $pdf['conditionalLogic'] ) || $this->misc->evaluate_conditional_logic( $pdf['conditionalLogic'], $entry ) ) ) {
				$filtered[ $pdf['id'] ] = $pdf;
			}
		}

		/**
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_get_active_pdfs/ for usage
		 *
		 * @since 4.2
		 */
		return apply_filters( 'gfpdf_get_active_pdfs', $filtered, $pdfs, $entry, $form );
	}

	/**
	 * Generate the PDF Name
	 *
	 * @param array $settings The PDF Form Settings
	 * @param array $entry    The Gravity Form entry details
	 *
	 * @return string      The PDF Name
	 *
	 * @since  4.0
	 */
	public function get_pdf_name( $settings, $entry ) {

		$form = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );
		$name = $this->gform->process_tags( $settings['filename'], $form, $entry );

		/* Decode HTML entities */
		$name = wp_specialchars_decode( $name, ENT_QUOTES );

		/*
		 * Add filter to modify PDF name
		 *
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_pdf_filename/ for more details about this filter
		 */
		$name = apply_filters( 'gfpdf_pdf_filename', $name, $form, $entry, $settings );

		/* Backwards compatible filter */
		$name = apply_filters( 'gfpdfe_pdf_filename', $name, $form, $entry, $settings );

		/* Remove any characters that cannot be present in a filename */
		$name = $this->misc->strip_invalid_characters( $name );

		return $name;
	}

	/**
	 * Create a PDF Link based on the current PDF settings and entry
	 *
	 * @param integer $pid          The PDF Form Settings ID
	 * @param integer $id           The Gravity Form entry ID
	 * @param boolean $download     Whether the PDF should be downloaded or not
	 * @param boolean $should_print Whether we should mark the PDF to be printed
	 * @param boolean $esc          Whether to escape the URL or not
	 *
	 * @return string       Direct link to the PDF
	 *
	 * @since  4.0
	 */
	public function get_pdf_url( $pid, $id, $download = false, $should_print = false, $esc = true ) {
		global $wp_rewrite;

		if ( $esc !== true ) {
			_doing_it_wrong( __METHOD__, '$esc has been deprecated. Late-escape the returned value where appropriate.', '6.4.0' );
		}

		/*
		 * Patch for WPML which can include the default language as a GET parameter
		 * See https://github.com/GravityPDF/gravity-pdf/issues/550
		 */
		$home_url = untrailingslashit( strtok( home_url(), '?' ) );

		/* Check if permalinks are enabled, otherwise fall back to our ugly link structure for 4.0 (not the same as our v3 links) */
		if ( $wp_rewrite->using_permalinks() ) {
			$url  = $home_url . '/' . $wp_rewrite->root; /* Handle "almost pretty" permalinks - fix for IIS servers without modrewrite  */
			$url .= 'pdf/' . $pid . '/' . $id . '/';

			if ( $download ) {
				$url .= 'download/';
			}

			$url = user_trailingslashit( $url );

			if ( $should_print ) {
				$url .= '?print=1';
			}
		} else {
			$url = $home_url . '/?gpdf=1&pid=' . $pid . '&lid=' . $id;

			if ( $download ) {
				$url .= '&action=download';
			}

			if ( $should_print ) {
				$url .= '&print=1';
			}
		}

		/*
		 * @since 4.2
		 */
		$url = apply_filters( 'gfpdf_get_pdf_url', $url, $pid, $id, $download, $should_print, $esc );

		return esc_url_raw( $url );
	}

	/**
	 * Display the PDF links on the entry detailed section of the admin area
	 *
	 * @param array $args Combined form and entry array
	 *
	 * @return void
	 *
	 * @since  4.0
	 */
	public function view_pdf_entry_detail( $args ) {
		$controller = $this->getController();
		$pdf_list   = $this->get_pdf_display_list( $args['entry'] );

		if ( empty( $pdf_list ) ) {
			$controller->view->entry_no_valid_pdf();

			return;
		}

		$pdfs = [
			'pdfs' => $pdf_list,
		];

		$controller->view->entry_detailed_pdf( $pdfs );
	}

	/**
	 * Display the PDF metabox in the Gravity Flow inbox
	 *
	 * @param array $form
	 * @param array $entry
	 * @param $current_step
	 * @param $args
	 *
	 * @return void
	 *
	 * @since 6.8
	 */
	public function view_pdf_gravityflow_inbox( $form, $entry, $current_step, $args ) {
		/* Only show the PDF metabox if a user has permission to view the documents */
		if ( ! $this->can_user_view_pdf_with_capabilities() ) {
			return;
		}

		$active_pdfs = array_filter(
			$form['gfpdf_form_settings'] ?? [],
			function ( $pdf ) {
				return $pdf['active'] === true;
			}
		);

		/* Only show the metabox if there's an active PDF */
		if ( count( $active_pdfs ) === 0 ) {
			return;
		}

		?>
		<style type="text/css">
		  div.gf_entry_wrap #poststuff #gravitypdf-pdf-box-container .inside {
			margin: 0;
			padding: 0;
			max-height: 18rem;
			overflow-y: auto;
			line-height: 1.4;
			font-size: 13px;
		  }

		  #gravitypdf-pdf-box-container ul {
			margin: 0;
			padding: 0;
		  }

		  #gravitypdf-pdf-box-container li {
			margin-bottom: 0.25rem;
			border-bottom: 1px solid #EBEBF2;
			padding: 0.5rem 0.75rem;
		  }

		  #gravitypdf-pdf-box-container li:last-of-type {
			border-bottom: none;
			margin-bottom: 0;
		  }
		</style>
		
		<div id="gravitypdf-pdf-box-container" class="postbox">

			<h3 class="hndle" style="cursor:default;">
				<span><?php esc_html_e( 'Gravity PDF', 'gravity-forms-pdf-extended' ); ?></span>
			</h3>

			<div class="inside">
				<?php $this->view_pdf_entry_detail( [ 'entry' => $entry ] ); ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Add the pdf meta box to the entry detail page.
	 *
	 * @param array $meta_boxes The properties for the meta boxes.
	 * @param array $entry      The entry currently being viewed/edited.
	 * @param array $form       The form object used to process the current entry.
	 *
	 * @return array
	 *
	 * @since 6.0
	 */
	public function register_pdf_meta_box( $meta_boxes, $entry, $form ) {

		$active_pdfs = array_filter(
			$form['gfpdf_form_settings'] ?? [],
			function ( $pdf ) {
				return $pdf['active'] === true;
			}
		);

		/* Don't display meta box if no active or valid PDFs for the form */
		if ( count( $active_pdfs ) === 0 ) {
			return $meta_boxes;
		}

		$meta = [
			'gfpdf-entry-details-list' => [
				'title'         => esc_html__( 'PDFs', 'gravity-forms-pdf-extended' ),
				'callback'      => function ( $args ) {
					/* Only show the PDF metabox if a user has permission to view the documents */
					if ( ! $this->can_user_view_pdf_with_capabilities() ) {
						return;
					}
					$this->view_pdf_entry_detail( $args );
				},
				'context'       => 'side',
				'callback_args' => [
					'form'  => $form,
					'entry' => $entry,
				],
			],
		];

		/* Ensure the PDF meta box is inserted right after the Entry box */

		return array_merge(
			array_slice( $meta_boxes, 0, 1 ),
			$meta,
			array_slice( $meta_boxes, 1 )
		);
	}

	/**
	 * Check if the form has any PDFs, generate them and attach to the notification
	 *
	 * @param array $notifications Gravity Forms Notification Array
	 * @param array $form
	 * @param array $entry
	 *
	 * @return array
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function notifications( $notifications, $form, $entry ) {

		/*
		 * Ensure our entry is stored in the database by checking it has an ID
		 * This resolves any issues with the "Save and Continue" feature
		 * See https://github.com/GravityPDF/gravity-pdf/issues/360
		 */
		if ( empty( $entry['id'] ) ) {
			return $notifications;
		}

		$pdfs = ( isset( $form['gfpdf_form_settings'] ) ) ? $this->get_active_pdfs( $form['gfpdf_form_settings'], $entry ) : [];

		if ( count( $pdfs ) > 0 ) {

			/* Ensure our notification has an array setup for the attachments key */
			$notifications['attachments'] = ( isset( $notifications['attachments'] ) ) ? $notifications['attachments'] : [];

			/* Loop through each PDF config and generate */
			foreach ( $pdfs as $pdf ) {

				/* Pass it through the config filters */
				$settings = $this->options->get_pdf( $entry['form_id'], $pdf['id'] );

				/* Reset the variables each loop */
				$filename        = '';
				$tier_2_filename = '';

				if ( $this->maybe_attach_to_notification( $notifications, $settings, $entry, $form ) ) {

					/* Generate our PDF */
					do_action( 'gfpdf_pre_generate_and_save_pdf_notification', $form, $entry, $settings, $notifications );
					$filename = $this->generate_and_save_pdf( $entry, $settings );
					do_action( 'gfpdf_post_generate_and_save_pdf_notification', $form, $entry, $settings, $notifications );

					if ( ! is_wp_error( $filename ) ) {
						$notifications['attachments'][] = $filename;
					}
				}
			}

			$this->log->notice(
				'Gravity Forms Attachments',
				[
					'attachments'  => $notifications['attachments'],
					'notification' => $notifications,
				]
			);
		}

		return $notifications;
	}

	/**
	 * Determine if the PDF should be attached to the current notification
	 *
	 * @param array $notification The Gravity Form Notification currently being processed
	 * @param array $settings     The current Gravity PDF Settings
	 * @param array $form         Added to 4.2
	 * @param array $entry        Added to 4.2
	 *
	 * @return boolean
	 *
	 * @since 4.0
	 */
	public function maybe_attach_to_notification( $notification, $settings, $entry = [], $form = [] ) {

		$attach = false;
		if ( isset( $settings['notification'] ) && is_array( $settings['notification'] ) ) {
			if ( in_array( $notification['id'], $settings['notification'], true ) ) {
				$attach = true;
			}
		}

		/**
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_maybe_attach_to_notification/ for usage
		 *
		 * @since 4.2
		 */
		return apply_filters( 'gfpdf_maybe_attach_to_notification', $attach, $notification, $settings, $entry, $form );
	}

	/**
	 * Generate and save the PDF to disk
	 *
	 * @param array $entry    The Gravity Form entry array (usually passed in as a filter or pulled using GFAPI::get_entry( $id ) )
	 * @param array $settings The PDF configuration settings for the particular entry / form being processed
	 *
	 * @return string|WP_Error           Return the full path to the PDF, or a WP_Error on failure
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function generate_and_save_pdf( $entry, $settings ) {

		$pdf_generator = new Helper_PDF( $entry, $settings, $this->gform, $this->data, $this->misc, $this->templates, $this->log );
		$pdf_generator->set_filename( $this->get_pdf_name( $settings, $entry ) );
		$pdf_generator = apply_filters( 'gfpdf_pdf_generator_pre_processing', $pdf_generator );

		if ( $this->process_and_save_pdf( $pdf_generator ) ) {
			$pdf_path = $pdf_generator->get_full_pdf_path();
			if ( is_file( $pdf_path ) ) {
				return $pdf_path;
			}
		}

		return new WP_Error( 'pdf_generation_failure', esc_html__( 'The PDF could not be saved.', 'gravity-forms-pdf-extended' ) );
	}

	/**
	 * Generate and save PDF to disk
	 *
	 * @param Helper_PDF $pdf The Helper_PDF object
	 *
	 * @return boolean
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function process_and_save_pdf( Helper_PDF $pdf ) {

		/**
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_override_pdf_bypass/ for usage
		 *
		 * @since 4.2
		 */
		$pdf_override = apply_filters( 'gfpdf_override_pdf_bypass', false, $pdf );

		/* Check that the PDF hasn't already been created this session */
		if ( $pdf_override || ! $this->does_pdf_exist( $pdf ) ) {

			/* Ensure Gravity Forms dependency loaded */
			$this->misc->maybe_load_gf_entry_detail_class();

			/* Enable Multicurrency support */
			$this->misc->maybe_add_multicurrency_support();

			/* Get required parameters */
			$entry    = $pdf->get_entry();
			$settings = $pdf->get_settings();
			$form     = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );

			do_action( 'gfpdf_pre_pdf_generation', $form, $entry, $settings, $pdf );

			/**
			 * Load our arguments that should be accessed by our PDF template
			 *
			 * @var array
			 */
			$args = $this->templates->get_template_arguments(
				$form,
				$this->misc->get_fields_sorted_by_id( $form['id'] ),
				$entry,
				$this->get_form_data( $entry ),
				$settings,
				$this->templates->get_config_class( $settings['template'] ),
				$this->misc->get_legacy_ids( $entry['id'], $settings )
			);

			/* Add backwards compatibility support */
			$GLOBALS['wp']->query_vars['pid'] = $settings['id'];
			$GLOBALS['wp']->query_vars['lid'] = $entry['id'];

			try {

				/* Initialise our PDF helper class */
				$pdf->init();
				$pdf->set_template();
				$pdf->set_output_type( 'save' );

				/* Add Backwards compatibility support for our v3 Tier 2 Add-on */
				if ( isset( $settings['advanced_template'] ) && strtolower( $settings['advanced_template'] ) === 'yes' ) {

					/* Check if we should process this document using our legacy system */
					if ( $this->handle_legacy_tier_2_processing( $pdf, $entry, $settings, $args ) ) {
						return true;
					}
				}

				/* Render the PDF template HTML */
				$pdf->render_html( $args );

				/* Generate and save the PDF */
				$pdf->save_pdf( $pdf->generate() );

				do_action( 'gfpdf_post_pdf_generation', $form, $entry, $settings, $pdf );

				return true;
			} catch ( Exception $e ) {

				$this->log->error(
					'PDF Generation Error',
					[
						'pdf'       => $pdf,
						'exception' => $e->getMessage(),
					]
				);

				return false;
			}
		}

		return true;
	}

	/**
	 * Check if the current PDF to be processed already exists on disk
	 *
	 * @param Helper_PDF $pdf The Helper_PDF Object
	 *
	 * @return boolean
	 *
	 * @since  4.0
	 */
	public function does_pdf_exist( Helper_PDF $pdf ) {

		if ( is_file( $pdf->get_full_pdf_path() ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Generates our $data array
	 *
	 * @param array $entry The Gravity Form Entry
	 *
	 * @return array        The $data array
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function get_form_data( $entry ) {

		$entry = apply_filters( 'gfpdf_entry_pre_form_data', $entry );

		if ( ! isset( $entry['form_id'] ) ) {
			return [];
		}

		$form = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );

		if ( ! is_array( $form ) ) {
			return [];
		}

		/* Setup our basic structure */
		$data = [
			'misc'               => [],
			'field'              => [],
			'field_descriptions' => [],
		];

		/**
		 * Create a product class for use
		 *
		 * @var Field_Products
		 */
		$products = new Field_Products( new GF_Field(), $entry, $this->gform, $this->misc );

		/* Get the form details */
		$form_meta = $this->get_form_data_meta( $form, $entry );

		/* Get the survey, quiz and poll data if applicable */
		$quiz   = $this->get_quiz_results( $form, $entry );
		$survey = $this->get_survey_results( $form, $entry );
		$poll   = $this->get_poll_results( $form, $entry );

		/* Merge in the meta data and survey, quiz and poll data */
		$data = array_replace_recursive( $data, $form_meta, $quiz, $survey, $poll );

		/*
		 * Loop through the form data, call the correct field object and
		 * save the data to our $data array
		 */
		if ( isset( $form['fields'] ) ) {
			foreach ( $form['fields'] as $field ) {

				/* Skip over captcha, password and page fields */
				$fields_to_skip = [ 'captcha', 'password', 'page' ];
				$fields_to_skip = apply_filters( 'gfpdf_form_data_skip_fields', $fields_to_skip );
				$fields_to_skip = apply_filters( 'gfpdf_blacklisted_fields', $fields_to_skip );

				if ( in_array( $field->type, $fields_to_skip, true ) ) {
					continue;
				}

				/* Include any field descriptions */
				$data['field_descriptions'][ $field->id ] = ( ! empty( $field->description ) ) ? $field->description : '';

				/* Get our field object */
				$class = $this->get_field_class( $field, $form, $entry, $products );

				/* Merge in the field object form_data() results */
				$data = array_replace_recursive( $data, $class->form_data() );
			}
		}

		/* Load our product array if products exist */
		if ( ! $products->is_empty() ) {
			$data = array_replace_recursive( $data, $products->form_data() );
		}

		/* Re-order the array keys to make it more readable */
		$order = apply_filters(
			'gfpdf_form_data_key_order',
			[
				'misc',
				'field',
				'list',
				'signature_details_id',
				'products',
				'products_totals',
				'repeater',
				'poll',
				'survey',
				'quiz',
				'pages',
				'html_id',
				'section_break',
				'field_descriptions',
				'signature',
				'signature_details',
				'html',
			]
		);

		foreach ( $order as $key ) {

			/* If item exists pop it onto the end of the array */
			if ( isset( $data[ $key ] ) ) {
				$item = $data[ $key ];
				unset( $data[ $key ] );
				$data[ $key ] = $item;
			}
		}

		/**
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_form_data/ for usage
		 *
		 * @since 4.2
		 */
		return apply_filters( 'gfpdf_form_data', $data, $entry, $form );
	}

	/**
	 * Handles the loading and running of our legacy Tier 2 PDF templates
	 *
	 * @param Helper_PDF $pdf      The Helper_PDF object
	 * @param array      $entry    The Gravity Forms raw entry data
	 * @param array      $settings The Gravity PDF settings
	 * @param array      $args     The data that should be passed directly to a PDF template
	 *
	 * @return bool
	 *
	 * @since 4.0
	 */
	public function handle_legacy_tier_2_processing( Helper_PDF $pdf, $entry, $settings, $args ) {

		$form = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );

		$prevent_main_pdf_loader = apply_filters(
			'gfpdfe_pre_load_template',
			$form['id'],
			$entry['id'],
			basename( $pdf->get_template_path() ),
			$form['id'] . $entry['id'],
			$this->misc->backwards_compat_output( $pdf->get_output_type() ),
			$pdf->get_filename(),
			$this->misc->backwards_compat_conversion( $settings, $form, $entry ),
			$args
		); /* Backwards Compatibility */

		return $prevent_main_pdf_loader === true;
	}

	/**
	 * Return our general $data information
	 *
	 * @param array $form  The Gravity Form
	 * @param array $entry The Gravity Form Entry
	 *
	 * @return array        The $data array
	 *
	 * @since 4.0
	 */
	public function get_form_data_meta( $form, $entry ) {
		$data = [];

		/* Add form_id and entry_id for convenience */
		$data['form_id']  = isset( $entry['form_id'] ) ? $entry['form_id'] : 0;
		$data['entry_id'] = isset( $entry['id'] ) ? $entry['id'] : 0;

		/* Set title and description */
		$data['form_title']       = isset( $form['title'] ) ? $form['title'] : '';
		$data['form_description'] = isset( $form['description'] ) ? $form['description'] : '';

		/* Include page names */
		$data['pages'] = isset( $form['pagination']['pages'] ) ? $form['pagination']['pages'] : [];

		/* Add date fields */
		if ( isset( $entry['date_created'] ) ) {
			$data['date_created']     = GFCommon::format_date( $entry['date_created'], false, 'j/n/Y', false );
			$data['date_created_usa'] = GFCommon::format_date( $entry['date_created'], false, 'n/j/Y', false );

			$data['misc']['date_time'] = GFCommon::format_date( $entry['date_created'], false, 'Y-m-d H:i:s', false );
			$data['misc']['time_24hr'] = GFCommon::format_date( $entry['date_created'], false, 'H:i', false );
			$data['misc']['time_12hr'] = GFCommon::format_date( $entry['date_created'], false, 'g:ia', false );
		}

		$include = [
			'is_starred',
			'is_read',
			'ip',
			'source_url',
			'post_id',
			'currency',
			'payment_status',
			'payment_date',
			'transaction_id',
			'payment_amount',
			'is_fulfilled',
			'created_by',
			'transaction_type',
			'user_agent',
			'status',
		];

		foreach ( $include as $item ) {
			$data['misc'][ $item ] = ( isset( $entry[ $item ] ) ) ? $entry[ $item ] : '';
		}

		return $data;
	}

	/**
	 * Pull the Quiz Results into the $form_data array
	 *
	 * @param array $form  The Gravity Form
	 * @param array $entry The Gravity Form Entry
	 *
	 * @return array        The results
	 *
	 * @since  4.0
	 */
	public function get_quiz_results( $form, $entry ) {

		$data = [];

		if ( class_exists( 'GFQuiz' ) && $this->check_field_exists( 'quiz', $form ) ) {

			/* Get quiz fields */
			$fields = GFCommon::get_fields_by_type( $form, [ 'quiz' ] );

			/* Store the quiz pass configuration */
			$data['quiz']['config']['grading']     = ( isset( $form['gravityformsquiz']['grading'] ) ) ? $form['gravityformsquiz']['grading'] : '';
			$data['quiz']['config']['passPercent'] = ( isset( $form['gravityformsquiz']['passPercent'] ) ) ? $form['gravityformsquiz']['passPercent'] : '';
			$data['quiz']['config']['grades']      = ( isset( $form['gravityformsquiz']['grades'] ) ) ? $form['gravityformsquiz']['grades'] : '';

			/* Store the user's quiz results */
			$data['quiz']['results']['score']   = rgar( $entry, 'gquiz_score' );
			$data['quiz']['results']['percent'] = rgar( $entry, 'gquiz_percent' );
			$data['quiz']['results']['is_pass'] = rgar( $entry, 'gquiz_is_pass' );
			$data['quiz']['results']['grade']   = rgar( $entry, 'gquiz_grade' );

			/* Poll for the global quiz overall results */
			$data['quiz']['global'] = $this->get_quiz_overall_data( $form, $fields );

		}

		return $data;
	}

	/**
	 * Pull the Survey Results into the $form_data array
	 *
	 * @param array $form  The Gravity Form
	 * @param array $entry The Gravity Form Entry
	 *
	 * @return array        The results
	 *
	 * @since  4.0
	 */
	public function get_survey_results( $form, $entry ) {

		$data = [];

		if ( class_exists( 'GFSurvey' ) && $this->check_field_exists( 'survey', $form ) ) {

			/* Get survey fields */
			$fields = GFCommon::get_fields_by_type( $form, [ 'survey' ] );

			/* Include the survey score, if any */
			if ( isset( $entry['gsurvey_score'] ) ) {
				$data['survey']['score'] = $entry['gsurvey_score'];
			}

			$results = $this->get_addon_global_data( $form, [], $fields );

			if ( count( $results ) > 0 ) {

				/* Loop through the global survey data and convert information correctly */
				foreach ( $fields as $field ) {

					/* Check if we have a multifield likert and replace the row key */
					if ( isset( $field['gsurveyLikertEnableMultipleRows'] ) && $field['gsurveyLikertEnableMultipleRows'] === true ) {

						foreach ( $field['gsurveyLikertRows'] as $row ) {

							$results['field_data'][ $field->id ] = $this->replace_key( $results['field_data'][ $field->id ], $row['value'], $row['text'] );

							if ( isset( $field->choices ) && is_array( $field->choices ) ) {
								foreach ( $field->choices as $choice ) {
									$results['field_data'][ $field->id ][ $row['text'] ] = $this->replace_key( $results['field_data'][ $field->id ][ $row['text'] ], $choice['value'], $choice['text'] );
								}
							}
						}
					}

					/* Replace the standard row data */
					if ( isset( $field->choices ) && is_array( $field->choices ) ) {
						foreach ( $field->choices as $choice ) {
							$results['field_data'][ $field->id ] = $this->replace_key( $results['field_data'][ $field->id ], $choice['value'], $choice['text'] );
						}
					}
				}

				$data['survey']['global'] = $results;
			}
		}

		return $data;
	}

	/**
	 * Pull the Poll Results into the $form_data array
	 *
	 * @param array $form  The Gravity Form
	 * @param array $entry The Gravity Form Entry
	 *
	 * @return array        The results
	 *
	 * @since  4.0
	 */
	public function get_poll_results( $form, $entry ) {

		$data = [];

		if ( class_exists( 'GFPolls' ) && $this->check_field_exists( 'poll', $form ) ) {

			/* Get poll fields and the overall results */
			$fields  = GFCommon::get_fields_by_type( $form, [ 'poll' ] );
			$results = $this->get_addon_global_data( $form, [], $fields );

			if ( count( $results ) > 0 ) {

				/* Loop through our fields and update the results as needed */
				foreach ( $fields as $field ) {

					/* Add the field name to a new 'misc' array key */
					$results['field_data'][ $field->id ]['misc']['label'] = $field->label;

					/* Loop through the field choices */
					foreach ( $field->choices as $choice ) {
						$results['field_data'][ $field->id ] = $this->replace_key( $results['field_data'][ $field->id ], $choice['value'], $choice['text'] );
					}
				}

				$data['poll']['global'] = $results;
			}
		}

		return $data;
	}

	/**
	 * Pass in a Gravity Form Field Object and get back a Gravity PDF Field Object
	 *
	 * @param GF_Field       $field    Gravity Form Field Object
	 * @param array          $form     The Gravity Form Array
	 * @param array          $entry    The Gravity Form Entry
	 * @param Field_Products $products A Field_Products Object
	 * @param array          $config   Should contain the keys 'meta' and 'settings'. Added in v6.9
	 *
	 * @return Helper_Abstract_Fields
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function get_field_class( $field, $form, $entry, Field_Products $products, $config = [] ) {

		$class_name = $this->misc->get_field_class( $field->type );

		try {
			/* if we have a valid class name... */
			if ( class_exists( $class_name ) ) {

				/**
				 * Developer Note
				 *
				 * We've purposefully not added any filters to the Field_* child classes directly.
				 * Instead, if you want to change how one of the fields are displayed or output (without effecting Gravity Forms itself) you should tap
				 * into one of the filters below and override or extend the entire class.
				 *
				 * Your class MUST extend the \GFPDF\Helper\Helper_Abstract_Fields abstract class - either directly or by extending an existing \GFPDF\Helper\Fields class.
				 * eg. class Fields_New_Text extends \GFPDF\Helper\Helper_Abstract_Fields or Fields_New_Text extends \GFPDF\Helper\Fields\Field_Text
				 *
				 * To make your life more simple you should either use the same namespace as the field classes (\GFPDF\Helper\Fields) or import the class directly (use \GFPDF\Helper\Fields\Field_Text)
				 * We've tried to make the fields as modular as possible. If you have any feedback about this approach please submit a ticket on GitHub (https://github.com/GravityPDF/gravity-pdf/issues)
				 */

				$class = new $class_name( $field, $entry, $this->gform, $this->misc );

				if ( $class instanceof Helper_Abstract_Field_Products ) {
					$class->set_products( $products );
				}

				/*
				 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_field_class/ for more details about these filters
				 */
				$class = apply_filters( 'gfpdf_field_class', $class, $field, $entry, $form );
				$class = apply_filters( 'gfpdf_field_class_' . $field->type, $class, $field, $entry, $form );
			}

			if ( empty( $class ) || ! ( $class instanceof Helper_Abstract_Fields ) ) {
				throw new Exception( 'Class not found' );
			}
		} catch ( Exception $e ) {

			$this->log->warning(
				sprintf(
					'Gravity PDF does not have native support for this field type "%s". Falling back to default Gravity Forms output.',
					$field->type
				),
				[
					'field' => $field,
				]
			);

			/* Exception thrown. Load generic field loader */
			$class = apply_filters( 'gfpdf_field_default_class', new Field_Default( $field, $entry, $this->gform, $this->misc ), $field, $entry, $form );
		}

		if ( $class instanceof Helper_Interface_Field_Pdf_Config ) {
			$class->set_pdf_config( $config );
		}

		return $class;
	}

	/**
	 * Sniff the form fields and determine if there are any of the $type available
	 *
	 * @param string $type the field type we are looking for
	 * @param array  $form the form array
	 *
	 * @return boolean       Whether there is a match or not
	 *
	 * @since 4.0
	 */
	public function check_field_exists( $type, $form ) {

		if ( isset( $form['fields'] ) ) {
			foreach ( $form['fields'] as $field ) {
				if ( $field['type'] === $type ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Parse the Quiz Overall Results
	 *
	 * @param array $form   The Gravity Form
	 * @param array $fields The quiz fields
	 *
	 * @return array         The parsed results
	 *
	 * @since 4.0
	 */
	public function get_quiz_overall_data( $form, $fields ) {

		if ( ! class_exists( 'GFQuiz' ) ) {
			return [];
		}

		/* GFQuiz is a singleton. Get the instance */
		$quiz = GFQuiz::get_instance();

		/* Create our callback to add additional data to the array specific to the quiz plugin */
		$options['callbacks']['calculation'] = [
			$quiz,
			'results_calculation',
		];

		$results = $this->get_addon_global_data( $form, $options, $fields );

		if ( count( $results ) > 0 ) {
			/* Loop through our fields and update our global results */
			foreach ( $fields as $field ) {

				/* Replace ['totals'] key with ['misc'] key */
				$results['field_data'][ $field->id ] = $this->replace_key( $results['field_data'][ $field->id ], 'totals', 'misc' );

				/* Add the field name to the ['misc'] key */
				$results['field_data'][ $field->id ]['misc']['label'] = $field->label;

				/* Loop through the field choices */
				if ( is_array( $field->choices ) ) {
					foreach ( $field->choices as $choice ) {
						$results['field_data'][ $field->id ] = $this->replace_key( $results['field_data'][ $field->id ], $choice['value'], $choice['text'] );

						/* Check if this is the correct field */
						if ( isset( $choice['gquizIsCorrect'] ) && $choice['gquizIsCorrect'] === true ) {
							$results['field_data'][ $field->id ]['misc']['correct_option_name'][] = esc_html( $choice['text'] );
						}
					}
				}
			}
		}

		return $results;
	}

	/**
	 * Pull Gravity Forms global results Data
	 *
	 * @param array $form    The Gravity Form array
	 * @param array $options The global query options
	 * @param array $fields  The field array to use in our query
	 *
	 * @return array          The results
	 *
	 * @since 4.0
	 */
	private function get_addon_global_data( $form, $options, $fields ) {

		/**
		 * Disable aggregate addon data (speeds up PDF generation time)
		 *
		 * See https://docs.gravitypdf.com/v6/developers/filters/gfpdf_disable_global_addon_data/
		 *
		 * @since 5.1
		 */
		if ( apply_filters( 'gfpdf_disable_global_addon_data', false, $form, $options, $fields ) ) {
			return [];
		}

		/* If the results class isn't loaded, load it */
		if ( ! class_exists( 'GFResults' ) ) {
			require_once GFCommon::get_base_path() . '/includes/addon/class-gf-results.php';
		}

		$form_id = $form['id'];

		/* Add form filter to keep in line with GF standard */
		$form = apply_filters( 'gform_form_pre_results', $form );
		$form = apply_filters( 'gform_form_pre_results_' . $form_id, $form );

		/* Initiate the results class */
		$gf_results = new GFResults( '', $options );

		/* Ensure that only active leads are queried */
		$search = [
			'field_filters' => [ 'mode' => '' ],
			'status'        => 'active',
		];

		/* Get the results */
		$data = $gf_results->get_results_data( $form, $fields, $search );

		/* Unset some array keys we don't need */
		unset( $data['status'] );
		unset( $data['timestamp'] );

		return $data;
	}

	/**
	 * Swap out the array key
	 *
	 * @param array  $array_to_modify The array to be modified
	 * @param string $key             The key to remove
	 * @param string $replacement_key The new array key
	 *
	 * @return array        The modified array
	 *
	 * @since 4.0
	 */
	public function replace_key( $array_to_modify, $key, $replacement_key ) {
		if ( $key !== $replacement_key && isset( $array_to_modify[ $key ] ) ) {

			/* Replace the array key with the actual field name */
			$array_to_modify[ $replacement_key ] = $array_to_modify[ $key ];
			unset( $array_to_modify[ $key ] );
		}

		return $array_to_modify;
	}

	/**
	 * Creates a PDF on every submission, except when the PDF is already created during the notification hook
	 *
	 * @param array $entry The GF Entry Details
	 * @param array $form  The Gravity Form
	 *
	 * @return void
	 *
	 * @throws Exception
	 * @since 4.0
	 */
	public function maybe_save_pdf( $entry, $form ) {

		/* Exit early if background processing is enabled */
		if ( $this->options->get_option( 'background_processing', 'No' ) === 'Yes' ) {
			return;
		}

		$pdfs = ( isset( $form['gfpdf_form_settings'] ) ) ? $this->get_active_pdfs( $form['gfpdf_form_settings'], $entry ) : [];

		if ( count( $pdfs ) > 0 ) {

			/* Loop through each PDF config */
			foreach ( $pdfs as $pdf ) {
				$settings = $this->options->get_pdf( $entry['form_id'], $pdf['id'] );

				/* Only generate if the PDF wasn't created during the notification process */
				if ( ! is_wp_error( $settings ) && $this->maybe_always_save_pdf( $settings, $entry['form_id'] ) ) {
					$this->generate_and_save_pdf( $entry, $settings );
				}
			}
		}
	}

	/**
	 * Determine if the PDF should be saved to disk
	 *
	 * @param array $settings The current Gravity PDF Settings
	 * @param int   $form_id  The current Form ID
	 *
	 * @since 4.0
	 */
	public function maybe_always_save_pdf( array $settings, int $form_id = 0 ): bool {

		$save = has_filter( 'gfpdf_post_save_pdf' ) || has_filter( 'gfpdf_post_save_pdf_' . $form_id );

		/* Legacy / Backwards compatible */
		if ( strtolower( $settings['save'] ?? '' ) === 'yes' ) {
			$save = true;
		}

		/**
		 * @since 4.2
		 */
		return apply_filters( 'gfpdf_maybe_always_save_pdf', $save, $settings, $form_id );
	}

	/**
	 * Trigger Post PDF Generation Action
	 *
	 * @param array      $form     The Gravity Form
	 * @param array      $entry    The Gravity Form Entry
	 * @param array      $settings The Gravity PDF Settings
	 * @param Helper_PDF $pdf      The Helper_PDF object
	 *
	 * @since 5.2
	 */
	public function trigger_post_save_pdf( $form, $entry, $settings, $pdf ) {
		$pdf_path = $pdf->get_full_pdf_path();

		if ( is_file( $pdf_path ) ) {
			/* Add appropriate filters so developers can access the PDF when it is generated */
			$form     = apply_filters( 'gfpdf_current_form_object', $this->gform->get_form( $entry['form_id'] ), $entry, __FUNCTION__ );
			$filename = basename( $pdf_path );

			do_action( 'gfpdf_post_pdf_save', $form['id'], $entry['id'], $settings, $pdf_path ); /* Backwards compatibility */

			/* See https://docs.gravitypdf.com/v6/developers/actions/gfpdf_post_save_pdf for more details about these actions */
			do_action( 'gfpdf_post_save_pdf', $pdf_path, $filename, $settings, $entry, $form );
			do_action( 'gfpdf_post_save_pdf_' . $form['id'], $pdf_path, $filename, $settings, $entry, $form );
		}
	}

	/**
	 * Clean-up our tmp directory every 12 hours
	 *
	 * @return void
	 *
	 * @since 4.0
	 */
	public function cleanup_tmp_dir() {
		$max_file_age  = time() - 12 * 3600; /* Max age is 12 hours old */
		$tmp_directory = $this->data->template_tmp_location;

		if ( is_dir( $tmp_directory ) ) {

			try {
				$directory_list = new RecursiveIteratorIterator(
					new RecursiveDirectoryIterator( $tmp_directory, RecursiveDirectoryIterator::SKIP_DOTS ),
					RecursiveIteratorIterator::CHILD_FIRST
				);

				foreach ( $directory_list as $file ) {
					if ( in_array( $file->getFilename(), [ '.htaccess', 'index.html' ], true ) || strpos( realpath( $file->getPathname() ), realpath( $this->data->mpdf_tmp_location ) ) !== false ) {
						continue;
					}

					if ( $file->isReadable() && $file->getMTime() < $max_file_age ) {
						( $file->isDir() ) ?
							$this->misc->rmdir( $file->getPathName() ) :
							@unlink( $file->getPathName() ); //phpcs:ignore
					}
				}
			} catch ( Exception $e ) {
				$this->log->error(
					'Filesystem Delete Error',
					[
						'dir'       => $tmp_directory,
						'exception' => $e->getMessage(),
					]
				);
			}
		}
	}

	/**
	 * Triggered after the Gravity Form entry is updated
	 *
	 * @param array $form
	 * @param int   $entry_id
	 */
	public function cleanup_pdf_after_submission( $form, $entry_id ) {
		/* Exit if background processing is enabled */
		if ( $this->options->get_option( 'background_processing', 'No' ) === 'Yes' ) {
			return;
		}

		$entry = $this->gform->get_entry( $entry_id );

		/* Exit if GF async notifications is enabled */
		$notifications = array_column( $form['notifications'] ?? [], 'id' );
		if ( $this->is_gform_asynchronous_notifications_enabled( $notifications, $form, $entry ) ) {
			return;
		}

		$this->cleanup_pdf( $entry, $form );
	}

	/**
	 * Remove the generated PDF from the server to save disk space
	 *
	 * @param array $entry The GF Entry Data
	 * @param array $form  The Gravity Form
	 *
	 * @return void
	 *
	 * @internal  In future we may give the option to cache PDFs to save on processing power
	 *
	 * @since     4.0
	 */
	public function cleanup_pdf( $entry, $form ) {
		$pdfs = $this->get_active_pdfs( $form['gfpdf_form_settings'] ?? [], $entry );

		if ( count( $pdfs ) === 0 ) {
			return;
		}

		$tmp_path_directory = realpath( $this->data->template_tmp_location );

		/* loop through each PDF config */
		foreach ( $pdfs as $pdf ) {
			$pdf_generator = new Helper_PDF( $entry, $pdf, $this->gform, $this->data, $this->misc, $this->templates, $this->log );
			$path          = $pdf_generator->get_path();

			/* Verify we are only deleting files in the designated tmp directory */
			$path_to_test = realpath( $path );
			if ( $path_to_test === false || strpos( $path_to_test, $tmp_path_directory ) !== 0 || ! is_dir( $path ) ) {
				continue;
			}

			$this->misc->rmdir( $path );
		}
	}

	/**
	 * Clean-up any PDFs stored on disk before we resend any notifications
	 *
	 * @param array $form    The Gravity Forms object
	 * @param array $entries An array of Gravity Form entry IDs
	 *
	 * @return array We tapped into a filter so we need to return the form object
	 * @since 4.0
	 *
	 */
	public function resend_notification_pdf_cleanup( $form, $entries ) {
		foreach ( $entries as $entry_id ) {
			$entry = $this->gform->get_entry( $entry_id );
			$this->cleanup_pdf( $entry, $form );
		}

		return $form;
	}

	/**
	 * Check if any of the form's notification is set to asynchronous
	 *
	 * @param array $notifications An array containing the IDs of the notifications to be sent.
	 * @param array $form          The form being processed.
	 * @param array $entry         The entry being processed.
	 * @param array $data          An array of data which can be used in the notifications via the generic {object:property} merge tag. Defaults to empty array.
	 *
	 * @return string
	 *
	 * @since 6.11.0
	 *
	 * @see   https://docs.gravityforms.com/gform_is_asynchronous_notifications_enabled/
	 */
	public function is_gform_asynchronous_notifications_enabled( $notifications, $form, $entry, $data = [] ) {
		return gf_apply_filters(
			[ 'gform_is_asynchronous_notifications_enabled', $form['id'] ],
			false,
			'form_submission',
			$notifications,
			$form,
			$entry,
			$data
		);
	}

	/**
	 * An mPDF filter which will register our custom font data with mPDF
	 *
	 * @param array $fonts The registered fonts
	 *
	 * @return array
	 *
	 * @since 4.0
	 */
	public function register_custom_font_data_with_mPDF( $fonts ) {

		$custom_fonts = $this->options->get_custom_fonts();

		foreach ( $custom_fonts as $font ) {

			$fonts[ $font['id'] ] = array_filter(
				[
					'R'          => basename( $font['regular'] ),
					'B'          => basename( $font['bold'] ),
					'I'          => basename( $font['italics'] ),
					'BI'         => basename( $font['bolditalics'] ),
					'useOTL'     => $font['useOTL'] ?? 0x00,
					'useKashida' => $font['useKashida'] ?? 0,
				]
			);
		}

		return $fonts;
	}

	/**
	 * Read all fonts from our fonts directory and auto-load them into mPDF if they are not found
	 *
	 * @param array $fonts The registered fonts
	 *
	 * @return array
	 * @since 4.0
	 */
	public function add_unregistered_fonts_to_mPDF( $fonts ) {

		$user_fonts = glob( $this->data->template_font_location . '*.[tT][tT][fF]', GLOB_NOSORT );
		$user_fonts = ( is_array( $user_fonts ) ) ? $user_fonts : [];

		$flattened_fonts_array = [];
		array_walk_recursive(
			$fonts,
			function ( $val ) use ( &$flattened_fonts_array ) {
				$flattened_fonts_array[] = $val;
			}
		);

		foreach ( $user_fonts as $font ) {

			/* Get font shortname */
			$font_name  = basename( $font );
			$short_name = $this->options->get_font_short_name( substr( $font_name, 0, -4 ) );

			/* Check if it exists already, otherwise add it */
			if ( ! isset( $fonts[ $short_name ] ) && array_search( $font_name, $flattened_fonts_array, true ) === false ) {
				$fonts[ $short_name ] = [
					'R' => $font_name,
				];
			}
		}

		return $fonts;
	}

	/**
	 * Attempts to find a configuration which matches the legacy routing method
	 *
	 * @param array $config
	 *
	 * @return mixed
	 *
	 * @since  4.0
	 */
	public function get_legacy_config( $config ) {

		/* Get the form settings */
		$pdfs = $this->options->get_form_pdfs( $config['fid'] );

		if ( is_wp_error( $pdfs ) ) {
			return $pdfs;
		}

		/* Reindex the $pdfs keys */
		$pdfs = array_values( $pdfs );

		/* Use the legacy aid to determine which PDF to load */
		if ( isset( $config['aid'] ) && $config['aid'] !== false ) {
			$selector = $config['aid'] - 1;

			if ( isset( $pdfs[ $selector ] ) && $pdfs[ $selector ]['template'] === $config['template'] ) {
				return $pdfs[ $selector ]['id'];
			}
		}

		/* The aid method failed so lets load the first matching configuration */
		foreach ( $pdfs as $pdf ) {
			if ( $pdf['active'] === true && $pdf['template'] === $config['template'] ) {
				return $pdf['id'];
			}
		}

		return new WP_Error( 'pdf_configuration_error', esc_html__( 'Could not find PDF configuration requested', 'gravity-forms-pdf-extended' ) );
	}

	/**
	 * Do any preprocessing to our arguments before they are sent to the template
	 *
	 * @param array $args
	 *
	 * @return array
	 *
	 * @since  4.0
	 */
	public function preprocess_template_arguments( $args ) {

		if ( isset( $args['settings']['header'] ) ) {
			$args['settings']['header'] = $this->gform->process_tags( $args['settings']['header'], $args['form'], $args['entry'] );
			$args['settings']['header'] = $this->misc->fix_header_footer( $args['settings']['header'] );
		}

		if ( isset( $args['settings']['first_header'] ) ) {
			$args['settings']['first_header'] = $this->gform->process_tags( $args['settings']['first_header'], $args['form'], $args['entry'] );
			$args['settings']['first_header'] = $this->misc->fix_header_footer( $args['settings']['first_header'] );
		}

		if ( isset( $args['settings']['footer'] ) ) {
			$args['settings']['footer'] = $this->gform->process_tags( $args['settings']['footer'], $args['form'], $args['entry'] );
			$args['settings']['footer'] = $this->misc->fix_header_footer( $args['settings']['footer'] );
		}

		if ( isset( $args['settings']['first_footer'] ) ) {
			$args['settings']['first_footer'] = $this->gform->process_tags( $args['settings']['first_footer'], $args['form'], $args['entry'] );
			$args['settings']['first_footer'] = $this->misc->fix_header_footer( $args['settings']['first_footer'] );
		}

		/**
		 * @since 4.2
		 */
		return apply_filters( 'gfpdf_preprocess_template_arguments', $args );
	}

	/**
	 * Skip over any fields with a class of "exclude"
	 *
	 * @param bool     $action
	 * @param GF_Field $field
	 * @param array    $entry
	 * @param array    $form
	 * @param array    $config
	 *
	 * @return bool
	 *
	 * @since 4.2
	 */
	public function field_middle_exclude( $action, $field, $entry, $form, $config ) {
		if ( $action === false ) {
			$skip_marked_fields = ( isset( $config['meta']['exclude'] ) ) ? $config['meta']['exclude'] : true;

			if ( $skip_marked_fields !== false && strpos( $field->cssClass, 'exclude' ) !== false ) {
				return true;
			}
		}

		return $action;
	}

	/**
	 * Determine if we should skip fields hidden with conditional logic
	 *
	 * @param bool     $action
	 * @param GF_Field $field
	 * @param array    $entry
	 * @param array    $form
	 * @param array    $config
	 *
	 * @return bool
	 *
	 * @since 4.2
	 */
	public function field_middle_conditional_fields( $action, $field, $entry, $form, $config ) {
		if ( $action === false ) {
			$skip_conditional_fields = ( isset( $config['meta']['conditional'] ) ) ? $config['meta']['conditional'] : true;
			if ( $skip_conditional_fields === true && GFFormsModel::is_field_hidden( $form, $field, [], $entry ) ) {
				return true;
			}
		}

		return $action;
	}

	/**
	 * Determine if we should skip product fields (by default they are grouped at the end of the form)
	 *
	 * @param bool     $action
	 * @param GF_Field $field
	 * @param array    $entry
	 * @param array    $form
	 * @param array    $config
	 *
	 * @return bool
	 *
	 * @since 4.2
	 */
	public function field_middle_product_fields( $action, $field, $entry, $form, $config ) {
		if ( $action === false ) {
			$show_individual_product_fields = ( isset( $config['meta']['individual_products'] ) ) ? $config['meta']['individual_products'] : false;
			if ( $show_individual_product_fields === false && GFCommon::is_product_field( $field->type ) ) {
				return true;
			}
		}

		return $action;
	}

	/**
	 * Determine if we should skip HTML fields
	 *
	 * @param bool     $action
	 * @param GF_Field $field
	 * @param array    $entry
	 * @param array    $form
	 * @param array    $config
	 *
	 * @return bool
	 *
	 * @since 4.2
	 */
	public function field_middle_html_fields( $action, $field, $entry, $form, $config ) {
		if ( $action === false ) {
			$show_html_fields = ( isset( $config['meta']['html_field'] ) ) ? $config['meta']['html_field'] : false;
			if ( $show_html_fields === false && $field->type === 'html' ) {
				return true;
			}
		}

		return $action;
	}

	/**
	 * Determine if we should skip Page fields
	 *
	 * @param bool     $action
	 * @param GF_Field $field
	 * @param array    $entry
	 * @param array    $form
	 * @param array    $config
	 *
	 * @return bool
	 *
	 * @since 6.10.1
	 */
	public function field_middle_page( $action, $field, $entry, $form, $config ) {
		if ( $action === false ) {
			$show_page_names = $config['meta']['page_names'] ?? false;
			if ( $show_page_names === false && $field->get_input_type() === 'page' ) {
				return true;
			}
		}

		return $action;
	}

	/**
	 * Check if the field is on our blacklist and skip
	 *
	 * @param bool           $action
	 * @param GF_Field       $field
	 * @param array          $entry
	 * @param array          $form
	 * @param array          $config
	 * @param Field_Products $products
	 * @param array          $blacklisted
	 *
	 * @return bool
	 *
	 * @since 4.2
	 */
	public function field_middle_blacklist( $action, $field, $entry, $form, $config, $products, $blacklisted ) {
		if ( $action === false ) {
			if ( in_array( $field->type, $blacklisted, true ) ) {
				return true;
			}
		}

		return $action;
	}

	/**
	 * Set the watermark font to the current PDF font
	 *
	 * @param Mpdf  $mpdf
	 * @param array $form
	 * @param array $entry
	 * @param array $settings
	 *
	 * @return Mpdf
	 *
	 * @since 5.0
	 */
	public function set_watermark_font( $mpdf, $form, $entry, $settings ) {
		$mpdf->watermark_font = ( isset( $settings['watermark_font'] ) ) ? $settings['watermark_font'] : $settings['font'];

		return $mpdf;
	}

	/**
	 * Replace any Gravity Perk Populate Anything live merge tags with their standard equivalent (i.e without the @ symbol)
	 * Include support for the `fallback` option
	 *
	 * @param string $text
	 * @param array  $form
	 * @param array  $entry
	 *
	 * @return string
	 *
	 * @since 5.3
	 */
	public function process_gp_populate_anything( $text, $form, $entry ) {
		$gp = GP_Populate_Anything_Live_Merge_Tags::get_instance();

		$this->disable_gp_populate_anything();
		$text = $gp->replace_live_merge_tags_static( $text, $form, $entry );
		$this->enable_gp_populate_anything();

		return $text;
	}

	/**
	 * At the end of the PDF generation, remove filter to replace merge tags for Gravity Perk Populate Anything
	 *
	 * @since 5.3
	 */
	public function disable_gp_populate_anything() {
		add_filter( 'gppa_allow_all_lmts', '__return_true' );
		remove_filter( 'gform_pre_replace_merge_tags', [ $this, 'process_gp_populate_anything' ] );
	}

	/**
	 * At the start of the PDF generation, filter all Gravity Perk Populate Anything merge tag replacement calls
	 *
	 * @since 5.3
	 */
	public function enable_gp_populate_anything() {
		remove_filter( 'gppa_allow_all_lmts', '__return_true' );
		add_filter( 'gform_pre_replace_merge_tags', [ $this, 'process_gp_populate_anything' ], 10, 3 );
	}

	/**
	 * Register Legal Signing path of additional font directory with mPDF
	 *
	 * @param array $config
	 *
	 * @return array
	 *
	 * @since
	 */
	public function register_legal_signing_font_path_with_mpdf( $config ) {
		if ( ! isset( $config['fontDir'] ) || ! is_array( $config['fontDir'] ) ) {
			$config['fontDir'] = [];
		}

		$config['fontDir'][] = WP_PLUGIN_DIR . '/' . dirname( FG_LEGALSIGNING_PLUGIN_BASENAME ) . '/dist/fonts/';

		return $config;
	}

	/**
	 * Register Legal Signing font files in mPDF
	 *
	 * @param array $fonts
	 *
	 * @return array
	 *
	 * @since 6.10
	 */
	public function register_legal_signing_fonts_with_mpdf( $fonts ) {
		$signature_fonts = glob( WP_PLUGIN_DIR . '/' . dirname( FG_LEGALSIGNING_PLUGIN_BASENAME ) . '/dist/fonts/*.[tT][tT][fF]', GLOB_NOSORT );
		$signature_fonts = is_array( $signature_fonts ) ? $signature_fonts : [];

		foreach ( $signature_fonts as $font ) {
			$font_id = basename( strtolower( $font ), '.ttf' );
			/* Skip if font ID already exists */
			if ( isset( $fonts[ $font_id ] ) ) {
				continue;
			}

			$fonts[ $font_id ] = [
				'R' => basename( $font ),
			];
		}

		return $fonts;
	}

	/**
	 * If the form has page fields, prepare for output in the PDF
	 *
	 * @param array $form
	 *
	 * @return array
	 *
	 * @since 6.10.1
	 */
	public function register_page_fields( $form ) {
		if ( ! isset( $form['pagination']['pages'][0] ) ) {
			return $form;
		}

		array_unshift(
			$form['fields'],
			new \GF_Field_Page(
				[
					'id'         => 0,
					'formId'     => $form['id'],
					'pageNumber' => 1,
					'cssClass'   => $form['firstPageCssClass'] ?? '',
				]
			)
		);

		array_map(
			function ( $item ) use ( $form ) {
				$item->label   = sprintf( esc_html__( 'Page %d', 'gravity-forms-pdf-extended' ), $item->pageNumber );
				$item->content = $form['pagination']['pages'][ $item->pageNumber - 1 ] ?? '';
			},
			\GFAPI::get_fields_by_type( $form, 'page', true )
		);

		return $form;
	}

	/**
	 * Hydrate the form with Populate Anything data
	 * For performance, the results are cached by the form/entry ID combo
	 *
	 * @param array $form
	 * @param array $entry
	 *
	 * @return array
	 *
	 * @since 6.10.2
	 */
	public function gp_populate_anything_hydrate_form( $form, $entry ) {
		static $cache = [];

		$form_id  = $form['id'] ?? '';
		$entry_id = $entry['id'] ?? '';

		$key = $form_id . $entry_id;

		if ( isset( $cache[ $key ] ) ) {
			return $cache[ $key ];
		}

		$hydrated_form = gp_populate_anything()->populate_form( $form, false, [], $entry );
		$cache[ $key ] = $hydrated_form;

		return $hydrated_form;
	}
}