From 96d553d6e69499baeae6f378fb045fc6a4d308d1 Mon Sep 17 00:00:00 2001 From: Zack Katz Date: Fri, 18 Jul 2025 16:54:30 -0400 Subject: [PATCH 1/2] Implement secure, basic caching This is step one to having a fully-better solution, but will greatly speed up sites for users accessing the same image multiple times. * **Improved file download security**: Added nonce verification for Gravity Forms file downloads accessed through GravityView to prevent unauthorized access and ensure downloads originate from legitimate requests. * **Enhanced file download caching**: Implemented intelligent caching for verified Gravity Forms file downloads, allowing browsers to cache files for up to one year while maintaining security through nonce validation. --- ...gravityview-plugin-hooks-gravity-forms.php | 129 ++++++++++++++++++ readme.txt | 1 + 2 files changed, 130 insertions(+) diff --git a/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php b/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php index a34b6043fc..48530e1e94 100644 --- a/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php +++ b/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php @@ -18,11 +18,48 @@ */ class GravityView_Plugin_Hooks_Gravity_Forms extends GravityView_Plugin_and_Theme_Hooks { + /** + * The query arg to identify the view. + * + * @since TODO + * + * @var string + */ + const QUERY_ARG_VIEW_ID = 'gvid'; + + /** + * The nonce query arg. + * + * @since TODO + * + * @var string + */ + const QUERY_ARG_NONCE = 'gvnonce'; + + /** + * The nonce action. + * + * @since TODO + * + * @var string + */ + const NONCE_ACTION = 'gvdownload'; + + /** + * @inheritDoc + * + * @since TODO + * + * @var string + */ public $class_name = 'GFForms'; /** * @inheritDoc + * * @since 1.15.2 + * + * @var array */ protected $style_handles = array( 'gform_tooltip', @@ -32,13 +69,105 @@ class GravityView_Plugin_Hooks_Gravity_Forms extends GravityView_Plugin_and_Them /** * @inheritDoc + * * @since 1.15.2 + * + * @var array */ protected $script_handles = array( 'gform_tooltip_init', 'gform_field_filter', 'gform_forms', ); + + /** + * @inheritDoc + * + * @since TODO + */ + public function __construct() { + parent::__construct(); + + if ( self::is_gf_gv_download() ) { + add_filter( 'nocache_headers', [ $this, 'remove_nocache_headers_from_gf_download' ], 1000 ); + } + } + + /** + * @inheritDoc + * + * @since TODO + */ + public function add_hooks() { + parent::add_hooks(); + + add_action( 'gravityview/template/before', [ $this, 'add_query_arg_to_gf_download_url' ] ); + } + + /** + * Checks if the current request is a Gravity Forms download coming from GravityView. + * + * @since TODO + * + * @return bool + */ + private static function is_gf_gv_download() { + + $is_download = ! empty( $_GET['gf-download'] ); + $nonce = \GV\Utils::get( $_GET, self::QUERY_ARG_NONCE, '' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $is_nonce_valid = $nonce && wp_verify_nonce( $nonce, self::NONCE_ACTION ); + + return $is_download && $is_nonce_valid; + } + + /** + * Adds a query arg to the Gravity Forms download URL to identify the View. + * + * This allows us to only remove the cache headers for GravityView embedded files. + * + * @param \GV\Template_Context $context The context object. + * + * @since TODO + */ + public function add_query_arg_to_gf_download_url( $context ) { + + /** + * Adds the View ID to the Gravity Forms download URL. + * + * @param string $url The existing Gravity Forms download URL. + * + * @return string The new Gravity Forms download URL, with the View ID added. + */ + add_filter( 'gform_secure_file_download_url', function( $url ) { + return wp_nonce_url( $url, self::NONCE_ACTION, self::QUERY_ARG_NONCE ); + } ); + } + + /** + * Remove cache headers for Gravity Forms downloads. + * + * @param array $headers + * + * @since TODO + * + * @return array + */ + public static function remove_nocache_headers_from_gf_download( $headers ) { + // Sanity check (this shouldn't be called if it's not already a GF download from GV). + if ( ! self::is_gf_gv_download() ) { + return $headers; + } + + // Nonces are valid for 24-48 hours. + $max_age = DAY_IN_SECONDS * 2; + + // Add caching headers to allow caching for as long as the nonce is valid. + $headers['Cache-Control'] = 'max-age=' . $max_age . ', public, immutable'; + $headers['Expires'] = gmdate( 'D, d M Y H:i:s', time() + $max_age ) . ' GMT'; + + return $headers; + } + } new GravityView_Plugin_Hooks_Gravity_Forms(); diff --git a/readme.txt b/readme.txt index b8f1f9deb9..b0fd7d76b9 100644 --- a/readme.txt +++ b/readme.txt @@ -24,6 +24,7 @@ Beautifully display your Gravity Forms entries. Learn more on [gravitykit.com](h = develop = * Added compatibility for Views embedded in Jetpack CRM Client Portal Pro pages. +* Improved performance of Gravity Forms secure embedded file image downloads embedded in Views by adding caching, while also maintaining security. = 2.42.2 on July 17, 2025 = From 5d455e946f970807c793ccb2588b15894a8a574b Mon Sep 17 00:00:00 2001 From: Zack Katz Date: Fri, 18 Jul 2025 17:02:49 -0400 Subject: [PATCH 2/2] Remove unused parameter [ci skip] --- .../class-gravityview-plugin-hooks-gravity-forms.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php b/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php index 48530e1e94..6f38bf0d71 100644 --- a/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php +++ b/includes/plugin-and-theme-hooks/class-gravityview-plugin-hooks-gravity-forms.php @@ -129,8 +129,7 @@ private static function is_gf_gv_download() { * * @since TODO */ - public function add_query_arg_to_gf_download_url( $context ) { - + public function add_query_arg_to_gf_download_url() { /** * Adds the View ID to the Gravity Forms download URL. *