diff --git a/plugins/woocommerce/includes/class-wc-product-download.php b/plugins/woocommerce/includes/class-wc-product-download.php index a0f47b516e7..a3176b7d5ca 100644 --- a/plugins/woocommerce/includes/class-wc-product-download.php +++ b/plugins/woocommerce/includes/class-wc-product-download.php @@ -201,7 +201,17 @@ class WC_Product_Download implements ArrayAccess { return; } - $download_file = $this->get_file(); + $download_file = $this->get_file(); + + /** + * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature. + * + * @param bool $should_validate + */ + if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $this->get_type_of_file_path() ) { + $download_file = do_shortcode( $download_file ); + } + $is_site_administrator = is_multisite() ? current_user_can( 'manage_sites' ) : current_user_can( 'manage_options' ); $valid_storage_directory = $download_directories->is_valid_path( $download_file ); @@ -216,7 +226,7 @@ class WC_Product_Download implements ArrayAccess { /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ __( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator and request their approval. %2$sLearn more.%3$s', 'woocommerce' ), '' . $download_file . '', - '', // @todo update to working link (see https://github.com/Automattic/woocommerce/issues/181) + '', '' ) ); @@ -229,7 +239,7 @@ class WC_Product_Download implements ArrayAccess { /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ __( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator for help. %2$sLearn more.%3$s', 'woocommerce' ), '' . $download_file . '', - '', // @todo update to working link (see https://github.com/Automattic/woocommerce/issues/181) + '', '' ) ); diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/legacy/js/admin/settings.js index 702b0ae1a19..29276907af6 100644 --- a/plugins/woocommerce/legacy/js/admin/settings.js +++ b/plugins/woocommerce/legacy/js/admin/settings.js @@ -70,8 +70,14 @@ // Edit prompt $( function() { var changed = false; + let $check_column = $( '.wp-list-table .check-column' ); + + $( 'input, textarea, select, checkbox' ).on( 'change', function( event ) { + // Toggling WP List Table checkboxes should not trigger navigation warnings. + if ( $check_column.length && $check_column.has( event.target ) ) { + return; + } - $( 'input, textarea, select, checkbox' ).on( 'change', function() { if ( ! changed ) { window.onbeforeunload = function() { return params.i18n_nav_warning; @@ -80,7 +86,7 @@ } }); - $( '.submit :input' ).on( 'click', function() { + $( '.submit :input, input#search-submit' ).on( 'click', function() { window.onbeforeunload = ''; }); }); diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php index 57ad552174a..3ef30a9b97e 100644 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php +++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/OptionRuleProcessor.php @@ -42,7 +42,7 @@ class OptionRuleProcessor implements RuleProcessorInterface { } if ( isset( $rule->transformers ) && is_array( $rule->transformers ) ) { - $option_value = TransformerService::apply( $option_value, $rule->transformers, $rule->default ); + $option_value = TransformerService::apply( $option_value, $rule->transformers, $default ); } return ComparisonOperation::compare( diff --git a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php index 0db1da8bd5e..d44c0404991 100644 --- a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php +++ b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php @@ -219,7 +219,18 @@ class Synchronize { $parent_url = _x( 'invalid URL', 'Approved product download URLs migration', 'woocommerce' ); try { - $parent_url = ( new URL( $downloadable->get_file() ) )->get_parent_url(); + $download_file = $downloadable->get_file(); + + /** + * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature. + * + * @param bool $should_validate + */ + if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $downloadable->get_type_of_file_path() ) { + $download_file = do_shortcode( $download_file ); + } + + $parent_url = ( new URL( $download_file ) )->get_parent_url(); $this->register->add_approved_directory( $parent_url, false ); } catch ( Exception $e ) { wc_get_logger()->log( diff --git a/plugins/woocommerce/src/Internal/Utilities/URL.php b/plugins/woocommerce/src/Internal/Utilities/URL.php index 340caa71c34..26b429e9f2a 100644 --- a/plugins/woocommerce/src/Internal/Utilities/URL.php +++ b/plugins/woocommerce/src/Internal/Utilities/URL.php @@ -300,8 +300,16 @@ class URL { $parent_path = '/' . $parent_path; } - // Form the parent URL, then process it exactly as we would any other URL for consistency. - $parent_url = $this->get_url( $this->get_path( $parent_path ) ); + // Form the parent URL (ditching the query and fragment, if set). + $parent_url = $this->get_url( + array( + 'path' => $parent_path, + 'query' => null, + 'fragment' => null, + ) + ); + + // We process the parent URL through a fresh instance of this class, for consistency. return ( new self( $parent_url ) )->get_url(); } @@ -310,27 +318,29 @@ class URL { * * Borrows from https://www.php.net/manual/en/function.parse-url.php#106731 * - * @param string $path_override If provided this will be used as the URL path. + * @param array $component_overrides If provided, these will override values set in $this->components. * * @return string */ - public function get_url( string $path_override = null ): string { - $scheme = null !== $this->components['scheme'] ? $this->components['scheme'] . '://' : ''; - $host = null !== $this->components['host'] ? $this->components['host'] : ''; - $port = null !== $this->components['port'] ? ':' . $this->components['port'] : ''; - $path = $path_override ?? $this->get_path(); + public function get_url( array $component_overrides = array() ): string { + $components = array_merge( $this->components, $component_overrides ); + + $scheme = null !== $components['scheme'] ? $components['scheme'] . '://' : ''; + $host = null !== $components['host'] ? $components['host'] : ''; + $port = null !== $components['port'] ? ':' . $components['port'] : ''; + $path = $this->get_path( $components['path'] ); // Special handling for hostless URLs (typically, filepaths) referencing the current working directory. if ( '' === $host && ( '' === $path || '.' === $path ) ) { $path = './'; } - $user = null !== $this->components['user'] ? $this->components['user'] : ''; - $pass = null !== $this->components['pass'] ? ':' . $this->components['pass'] : ''; + $user = null !== $components['user'] ? $components['user'] : ''; + $pass = null !== $components['pass'] ? ':' . $components['pass'] : ''; $user_pass = ( ! empty( $user ) || ! empty( $pass ) ) ? $user . $pass . '@' : ''; - $query = null !== $this->components['query'] ? '?' . $this->components['query'] : ''; - $fragment = null !== $this->components['fragment'] ? '#' . $this->components['fragment'] : ''; + $query = null !== $components['query'] ? '?' . $components['query'] : ''; + $fragment = null !== $components['fragment'] ? '#' . $components['fragment'] : ''; return $scheme . $user_pass . $host . $port . $path . $query . $fragment; } diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php index 0cb0ad4dc15..ba3d9eb484e 100644 --- a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php +++ b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php @@ -63,4 +63,48 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { $this->expectExceptionMessage( 'cannot be used: it is not located in an approved directory' ); $download->check_is_valid(); } + + /** + * Test handling of filepaths described via shortcodes in relation to the Approved Download Directory + * feature. This is to simulate scenarios such as encountered when using the S3 Downloads extension. + */ + public function test_shortcode_resolution_for_approved_directory_rules() { + /** @var Download_Directories $download_directories */ + $download_directories = wc_get_container()->get( Download_Directories::class ); + $download_directories->set_mode( Download_Directories::MODE_ENABLED ); + $dynamic_filepath = 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; + + // We select an admin user because we wish to automatically add Approved Directory rules. + $admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'administrator', 'user_pass' => 'x' ) ); + wp_set_current_user( $admin_user ); + + add_shortcode( 'dynamic-download', function () { + return 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; + } ); + + $this->assertFalse( + $download_directories->is_valid_path( $dynamic_filepath ), + 'Confirm the filepath returned by the test URL is not yet valid.' + ); + + $download = new WC_Product_Download(); + $download->set_file( '[dynamic-download]' ); + + $this->assertNull( + $download->check_is_valid(), + 'The downloadable file successfully validates (if it did not, an exception would be thrown).' + ); + + $this->assertTrue( + $download_directories->is_valid_path( $dynamic_filepath ), + 'Confirm the filepath returned by the test URL is now considered valid.' + ); + + remove_shortcode( 'dynamic-download' ); + + // Now the shortcode is removed (perhaps the parent plugin has been removed/disabled) it will not resolve + // and so the filepath will not validate. + $this->expectException( 'Error' ); + $download_directories->check_is_valid(); + } }