diff --git a/.distignore b/.distignore index 77adfeb..c36ebf5 100644 --- a/.distignore +++ b/.distignore @@ -8,6 +8,7 @@ /.wordpress-org /tests +.phpunit.result.cache /node_modules /build /playwright-report diff --git a/languages/perform.pot b/languages/perform.pot index e6c4ee3..8eee6dd 100644 --- a/languages/perform.pot +++ b/languages/perform.pot @@ -7,7 +7,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language-Team: PerformWP \n" -"POT-Creation-Date: 2026-05-21 10:19+0000\n" +"POT-Creation-Date: 2026-05-22 07:31+0000\n" "Report-Msgid-Bugs-To: https://github.com/performwp/perform/issues/new\n" "X-Poedit-Basepath: ..\n" "X-Poedit-KeywordsList: __;_e;_ex:1,2c;_n:1,2;_n_noop:1,2;_nx:1,2,4c;_nx_noop:1,2,3c;_x:1,2c;esc_attr__;esc_attr_e;esc_attr_x:1,2c;esc_html__;esc_html_e;esc_html_x:1,2c\n" @@ -16,19 +16,19 @@ msgstr "" "X-Poedit-SourceCharset: UTF-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/Admin/Actions.php:104 +#: src/Admin/Actions.php:96 msgid "Close Assets Manager" msgstr "" -#: src/Admin/Actions.php:102, src/Modules/Assets/AssetsManager.php:141 +#: src/Admin/Actions.php:93, src/Modules/Assets/AssetsManager.php:195 msgid "Assets Manager" msgstr "" -#: src/Admin/Actions.php:111, src/Admin/Settings/Menu.php:40, src/Admin/Settings/Menu.php:41, src/Modules/Assets/AssetsManager.php:124 +#: src/Admin/Actions.php:103, src/Admin/Settings/Menu.php:40, src/Admin/Settings/Menu.php:41, src/Modules/Assets/AssetsManager.php:178 msgid "Perform" msgstr "" -#: src/Admin/Actions.php:131 +#: src/Admin/Actions.php:123 msgid "Support Forum" msgstr "" @@ -568,170 +568,170 @@ msgstr "" msgid "Security check failed." msgstr "" -#: src/Admin/Settings/Menu.php:194 +#: src/Admin/Settings/Menu.php:203 msgid "Unable to save the settings. Please try again." msgstr "" -#: src/Admin/Settings/Menu.php:189 +#: src/Admin/Settings/Menu.php:196 msgid "Settings saved successfully." msgstr "" -#: src/Modules/Assets/AssetsManager.php:103 +#: src/Modules/Assets/AssetsManager.php:157 msgid "Search detected assets" msgstr "" -#: src/Modules/Assets/AssetsManager.php:104 +#: src/Modules/Assets/AssetsManager.php:158 msgid "Search by handle, source, or file URL" msgstr "" -#: src/Modules/Assets/AssetsManager.php:105 +#: src/Modules/Assets/AssetsManager.php:159 msgid "Filter detected assets" msgstr "" -#: src/Modules/Assets/AssetsManager.php:106 +#: src/Modules/Assets/AssetsManager.php:160 msgid "All" msgstr "" -#: src/Modules/Assets/AssetsManager.php:107 +#: src/Modules/Assets/AssetsManager.php:161 msgid "Plugins" msgstr "" -#: src/Modules/Assets/AssetsManager.php:108 +#: src/Modules/Assets/AssetsManager.php:162 msgid "Themes" msgstr "" -#: src/Modules/Assets/AssetsManager.php:109, src/Modules/Assets/AssetsManager.php:493 +#: src/Modules/Assets/AssetsManager.php:163, src/Modules/Assets/AssetsManager.php:547 msgid "Other" msgstr "" -#: src/Modules/Assets/AssetsManager.php:110 +#: src/Modules/Assets/AssetsManager.php:164 msgid "JS" msgstr "" -#: src/Modules/Assets/AssetsManager.php:111, src/Modules/Assets/AssetsManager.php:159 +#: src/Modules/Assets/AssetsManager.php:165, src/Modules/Assets/AssetsManager.php:213 msgid "CSS" msgstr "" -#: src/Modules/Assets/AssetsManager.php:112 +#: src/Modules/Assets/AssetsManager.php:166 msgid "Disabled" msgstr "" -#: src/Modules/Assets/AssetsManager.php:126 +#: src/Modules/Assets/AssetsManager.php:180 msgid "Page asset scanner" msgstr "" -#: src/Modules/Assets/AssetsManager.php:127 +#: src/Modules/Assets/AssetsManager.php:181 msgid "Perform Assets Manager" msgstr "" -#: src/Modules/Assets/AssetsManager.php:131 +#: src/Modules/Assets/AssetsManager.php:185 msgid "Close" msgstr "" -#: src/Modules/Assets/AssetsManager.php:132 +#: src/Modules/Assets/AssetsManager.php:186 msgid "Save changes" msgstr "" -#: src/Modules/Assets/AssetsManager.php:138 +#: src/Modules/Assets/AssetsManager.php:192 msgid "Current page" msgstr "" -#: src/Modules/Assets/AssetsManager.php:144 +#: src/Modules/Assets/AssetsManager.php:198 msgid "Review scripts and styles detected on this page, then disable only the assets you have verified are not needed." msgstr "" -#: src/Modules/Assets/AssetsManager.php:148 +#: src/Modules/Assets/AssetsManager.php:202 msgid "Asset scan summary" msgstr "" -#: src/Modules/Assets/AssetsManager.php:151 +#: src/Modules/Assets/AssetsManager.php:205 msgid "Detected assets" msgstr "" -#: src/Modules/Assets/AssetsManager.php:155 +#: src/Modules/Assets/AssetsManager.php:209 msgid "JavaScript" msgstr "" -#: src/Modules/Assets/AssetsManager.php:163 +#: src/Modules/Assets/AssetsManager.php:217 msgid "Disabled rules" msgstr "" -#: src/Modules/Assets/AssetsManager.php:167 +#: src/Modules/Assets/AssetsManager.php:221 msgid "Known file size" msgstr "" -#: src/Modules/Assets/AssetsManager.php:204 +#: src/Modules/Assets/AssetsManager.php:258 msgid "No assets match the current scan filter." msgstr "" -#: src/Modules/Assets/AssetsManager.php:494 +#: src/Modules/Assets/AssetsManager.php:548 msgid "WordPress core, CDN, and uncategorized assets" msgstr "" #. translators: %d: number of assets. -#: src/Modules/Assets/AssetsManager.php:499, src/Modules/Assets/AssetsManager.php:478 +#: src/Modules/Assets/AssetsManager.php:553, src/Modules/Assets/AssetsManager.php:532 msgid "%d detected asset" msgid_plural "%d detected assets" msgstr[0] "" msgstr[1] "" -#: src/Modules/Assets/AssetsManager.php:513 +#: src/Modules/Assets/AssetsManager.php:567 msgid "Handle" msgstr "" -#: src/Modules/Assets/AssetsManager.php:516 +#: src/Modules/Assets/AssetsManager.php:570 msgid "Type" msgstr "" -#: src/Modules/Assets/AssetsManager.php:519 +#: src/Modules/Assets/AssetsManager.php:573 msgid "Size" msgstr "" -#: src/Modules/Assets/AssetsManager.php:522 +#: src/Modules/Assets/AssetsManager.php:576 msgid "Status" msgstr "" -#: src/Modules/Assets/AssetsManager.php:525 +#: src/Modules/Assets/AssetsManager.php:579 msgid "Actions" msgstr "" -#: src/Modules/Assets/AssetsManager.php:583 +#: src/Modules/Assets/AssetsManager.php:637 msgid "External" msgstr "" -#: src/Modules/Assets/AssetsManager.php:591 +#: src/Modules/Assets/AssetsManager.php:645 msgid "View File" msgstr "" -#: src/Modules/Assets/AssetsManager.php:638 +#: src/Modules/Assets/AssetsManager.php:692 msgid "All assets in this group have been disabled. Enable the group again to manage individual assets." msgstr "" -#: src/Modules/Assets/AssetsManager.php:659, src/Modules/Assets/AssetsManager.php:749 +#: src/Modules/Assets/AssetsManager.php:713, src/Modules/Assets/AssetsManager.php:802 msgid "Current URL" msgstr "" -#: src/Modules/Assets/AssetsManager.php:660 +#: src/Modules/Assets/AssetsManager.php:714 msgid "Everywhere" msgstr "" -#: src/Modules/Assets/AssetsManager.php:665 +#: src/Modules/Assets/AssetsManager.php:719 msgid "Disable on" msgstr "" -#: src/Modules/Assets/AssetsManager.php:711 +#: src/Modules/Assets/AssetsManager.php:764 msgid "Asset loading status" msgstr "" -#: src/Modules/Assets/AssetsManager.php:713 +#: src/Modules/Assets/AssetsManager.php:766 msgid "ON" msgstr "" -#: src/Modules/Assets/AssetsManager.php:716 +#: src/Modules/Assets/AssetsManager.php:769 msgid "OFF" msgstr "" -#: src/Modules/Assets/AssetsManager.php:742 +#: src/Modules/Assets/AssetsManager.php:795 msgid "Exceptions" msgstr "" @@ -743,66 +743,66 @@ msgstr "" msgid "Every 5 minutes (Perform Cache)" msgstr "" -#: src/Modules/Cache/PageCache.php:447, src/Modules/Cache/PageCache.php:476 +#: src/Modules/Cache/PageCache.php:456, src/Modules/Cache/PageCache.php:485 msgid "Perform Cache Observability" msgstr "" -#: src/Modules/Cache/PageCache.php:448 +#: src/Modules/Cache/PageCache.php:457 msgid "Perform Cache Stats" msgstr "" -#: src/Modules/Cache/PageCache.php:477 +#: src/Modules/Cache/PageCache.php:486 msgid "Live cache effectiveness and warmup health metrics." msgstr "" -#: src/Modules/Cache/PageCache.php:481 +#: src/Modules/Cache/PageCache.php:490 msgid "Cache Hit Ratio" msgstr "" -#: src/Modules/Cache/PageCache.php:482 +#: src/Modules/Cache/PageCache.php:491 msgid "Hits" msgstr "" -#: src/Modules/Cache/PageCache.php:483 +#: src/Modules/Cache/PageCache.php:492 msgid "Stale Hits" msgstr "" -#: src/Modules/Cache/PageCache.php:484, src/Modules/Cache/PageCache.php:493 +#: src/Modules/Cache/PageCache.php:493, src/Modules/Cache/PageCache.php:502 msgid "Misses" msgstr "" -#: src/Modules/Cache/PageCache.php:485 +#: src/Modules/Cache/PageCache.php:494 msgid "Bypasses" msgstr "" -#: src/Modules/Cache/PageCache.php:486 +#: src/Modules/Cache/PageCache.php:495 msgid "Lock Waits" msgstr "" -#: src/Modules/Cache/PageCache.php:487 +#: src/Modules/Cache/PageCache.php:496 msgid "Preload Queue Size" msgstr "" -#: src/Modules/Cache/PageCache.php:488 +#: src/Modules/Cache/PageCache.php:497 msgid "Preload Requests" msgstr "" -#: src/Modules/Cache/PageCache.php:492 +#: src/Modules/Cache/PageCache.php:501 msgid "Top Missed URLs" msgstr "" -#: src/Modules/Cache/PageCache.php:495 +#: src/Modules/Cache/PageCache.php:504 msgid "Slow Uncached URLs (ms)" msgstr "" -#: src/Modules/Cache/PageCache.php:496 +#: src/Modules/Cache/PageCache.php:505 msgid "Render Time (ms)" msgstr "" -#: src/Modules/Cache/PageCache.php:511 +#: src/Modules/Cache/PageCache.php:520 msgid "No data yet." msgstr "" -#: src/Modules/Cache/PageCache.php:518 +#: src/Modules/Cache/PageCache.php:527 msgid "URL" msgstr "" diff --git a/package.json b/package.json index a1932da..8ee0686 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint": "npm run lint:js && npm run lint:css", "lint:css": "wp-scripts lint-style \"assets/src/**/*.css\"", "lint:js": "wp-scripts lint-js \"assets/src/**/*.js\" \"assets/src/**/*.jsx\" postcss.config.js webpack.config.js", - "plugin-zip": "rm -rf build/perform perform.zip && mkdir -p build/perform && rsync -rc --exclude-from=.distignore ./ build/perform/ --delete --delete-excluded && cd build && zip -qr ../perform.zip perform", + "plugin-zip": "rm -rf build/perform perform.zip && mkdir -p build/perform && rsync -rc --exclude-from=.distignore ./ build/perform/ --delete --delete-excluded && cp composer.json composer.lock build/perform/ && composer install --working-dir=build/perform --no-dev --prefer-dist --optimize-autoloader --no-interaction --no-progress --no-scripts && rm -f build/perform/composer.json build/perform/composer.lock && rm -rf build/perform/vendor/bin build/perform/vendor/composer/installers && cd build && zip -qr ../perform.zip perform && cd .. && rm -rf build/perform", "test:e2e": "playwright test", "test:e2e:ci": "node tests/e2e/run-ci.mjs", "wp-env": "wp-env" diff --git a/readme.txt b/readme.txt index 02abafd..06f4b19 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Donate link: https://www.buymeacoffee.com/mehulgohil Requires at least: 4.8 Tested up to: 6.9 Requires PHP: 7.4 -Stable tag: 1.5.1 +Stable tag: 1.6.0 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -53,8 +53,8 @@ Other Plugins = Will Perform break my theme or plugins? = Perform is conservative by default: it only disables assets when you explicitly choose them in the Assets Manager. If you disable something and see issues, re-enable the asset. Changes are reversible. -= Is this compatible with caching plugins like WP Rocket? = -Yes. Perform works alongside caching plugins and most server-level caching solutions. Clear cache after making asset changes. += Is this compatible with other caching plugins? = +Yes. Perform works alongside many caching plugins and most server-level caching solutions. Clear cache after making asset changes. = Which page builders are supported? = Full compatibility with majority of all the page builders. @@ -66,6 +66,16 @@ Contributions and bug reports welcome on GitHub: https://github.com/performwp/pe == Changelog == += 1.6.0 - 2026-05-22 = +- Redesigned Assets Manager with a more resilient scanner interface and WordPress-native admin controls. +- Added full-page cache controls with safer cache writes, response validation, stale regeneration, preload scheduling, and observability stats. +- Improved settings storage compatibility by preserving existing option keys while migrating legacy settings into the consolidated settings shape. +- Improved release validation with PHPUnit, PHPStan, JavaScript/CSS linting, production build checks, Playwright smoke coverage, and Node 24 tooling. +- Changed Menu Cache to run on classic themes by default, with a developer filter for hybrid themes that still render classic menus. +- Fixed Assets Manager save handling for current-page exceptions, missing option indexes, and admin-only frontend overlay assets. +- Fixed public feed compatibility when hiding the WordPress version. +- Fixed uninstall cleanup so multisite removals include Perform runtime cache and Assets Manager options. + = 1.5.1 - 2025-12-06 = - Added compatibility to WordPress 6.9 - Upgraded Freemius SDK to 2.13 @@ -102,6 +112,9 @@ Contributions and bug reports welcome on GitHub: https://github.com/performwp/pe == Upgrade Notice == += 1.6.0 = +Review your Assets Manager and cache settings after updating. Perform 1.6.0 adds the redesigned scanner, page cache controls, and safer release validation. + Always backup your database before updating. Follow the changelog for breaking changes. == Screenshots == @@ -114,5 +127,3 @@ Always backup your database before updating. Follow the changelog for breaking c == Contributors == performwp, mehul0810, ankur0812 - - diff --git a/src/Admin/Actions.php b/src/Admin/Actions.php index 1a8b729..e055456 100644 --- a/src/Admin/Actions.php +++ b/src/Admin/Actions.php @@ -39,6 +39,7 @@ public function __construct() { * * @return void */ + // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Preserve existing public method name for release compatibility. public function registerAssets() { $screen = get_current_screen(); if ( empty( $screen ) || 'settings_page_perform_settings' !== $screen->id ) { @@ -58,15 +59,15 @@ public function registerAssets() { 'perform-admin', 'performwpSettings', [ - 'version' => defined('PERFORM_VERSION') ? PERFORM_VERSION : '', - 'docsUrl' => defined('PERFORM_PLUGIN_DOCS_URL') ? PERFORM_PLUGIN_DOCS_URL : 'https://performwp.com/docs/', - 'logoUrl' => plugins_url( 'assets/dist/images/logo.png', PERFORM_PLUGIN_FILE ), - 'nonce' => wp_create_nonce( 'perform_save_settings' ), - 'saved' => ClientPayload::sanitize_for_client( (array) \Perform\Includes\Helpers::get_settings() ), - 'sensitiveKeys' => ClientPayload::get_sensitive_keys(), + 'version' => defined( 'PERFORM_VERSION' ) ? PERFORM_VERSION : '', + 'docsUrl' => defined( 'PERFORM_PLUGIN_DOCS_URL' ) ? PERFORM_PLUGIN_DOCS_URL : 'https://performwp.com/docs/', + 'logoUrl' => plugins_url( 'assets/dist/images/logo.png', PERFORM_PLUGIN_FILE ), + 'nonce' => wp_create_nonce( 'perform_save_settings' ), + 'saved' => ClientPayload::sanitize_for_client( (array) \Perform\Includes\Helpers::get_settings() ), + 'sensitiveKeys' => ClientPayload::get_sensitive_keys(), 'maskedSecretValue' => ClientPayload::MASKED_SECRET, - 'tabs' => \Perform\Includes\Helpers::get_settings_tabs(), - 'fields' => \Perform\Includes\Helpers::get_settings_fields(), // Expose fields to JS + 'tabs' => \Perform\Includes\Helpers::get_settings_tabs(), + 'fields' => \Perform\Includes\Helpers::get_settings_fields(), // Expose fields to JS ] ); } @@ -87,20 +88,11 @@ public function add_to_admin_bar( $wp_admin_bar ) { return; } - global $wp; - - $server_data = Helpers::clean( filter_input_array( INPUT_SERVER ) ); - - $href = add_query_arg( - str_replace( [ '&perform', 'perform' ], '', $server_data['QUERY_STRING'] ), - '', - home_url( $wp->request ) - ); - if ( ! isset( $_GET['perform'] ) ) { - $href .= ! empty( $server_data['QUERY_STRING'] ) ? '&perform' : '?perform'; + $href = add_query_arg( 'perform', '1' ); $menu_text = esc_html__( 'Assets Manager', 'perform' ); } else { + $href = remove_query_arg( 'perform' ); $menu_text = esc_html__( 'Close Assets Manager', 'perform' ); } diff --git a/src/Admin/Settings/Menu.php b/src/Admin/Settings/Menu.php index 510912e..5cbb07d 100644 --- a/src/Admin/Settings/Menu.php +++ b/src/Admin/Settings/Menu.php @@ -185,6 +185,9 @@ public function save_settings() { $new_settings['preconnect'] = $this->normalize_multiline_setting( $new_settings['preconnect'] ?? '' ); $is_saved = update_option( 'perform_settings', $new_settings, false ); + if ( ! $is_saved && get_option( 'perform_settings' ) === $new_settings ) { + $is_saved = true; + } if ( $is_saved ) { wp_send_json_success( diff --git a/src/Modules/Basic/DisableSelfPingbacks.php b/src/Modules/Basic/DisableSelfPingbacks.php index 1d2c98f..f9e9f96 100644 --- a/src/Modules/Basic/DisableSelfPingbacks.php +++ b/src/Modules/Basic/DisableSelfPingbacks.php @@ -23,7 +23,7 @@ * @since 2.0.0 */ class DisableSelfPingbacks extends AbstractModule { - protected static $option_key = 'disable_self_pingbacks'; + protected static $option_key = 'disable_self_pingbacks'; /** * Register hooks and filters for this module. @@ -44,7 +44,7 @@ public function register(): void { * * @return void */ - public function disable_self_pingbacks( $links ) { + public function disable_self_pingbacks( &$links ) { $home = get_option( 'home' ); foreach ( $links as $key => $link ) { diff --git a/src/Modules/Cache/PageCache.php b/src/Modules/Cache/PageCache.php index 69c144d..00000e1 100644 --- a/src/Modules/Cache/PageCache.php +++ b/src/Modules/Cache/PageCache.php @@ -200,6 +200,11 @@ public function maybe_serve_cache() { } } + if ( $is_regen_request ) { + $this->should_write_cache = true; + return; + } + // Miss path: acquire lock to avoid stampede. if ( ! $this->acquire_lock() ) { $this->increment_stat( 'lock_waits' ); @@ -776,7 +781,10 @@ private function maybe_trigger_async_regeneration( $url ) { return; } - set_transient( $this->lock_key, 1, 30 ); + $regen_token = $this->create_regen_token( $url ); + if ( ! set_transient( $this->lock_key, $regen_token, 30 ) ) { + return; + } wp_remote_post( esc_url_raw( $url ), @@ -785,12 +793,23 @@ private function maybe_trigger_async_regeneration( $url ) { 'blocking' => false, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), 'headers' => [ - 'X-Perform-Cache-Regen' => '1', + 'X-Perform-Cache-Regen' => $regen_token, ], ] ); } + /** + * Create an opaque token for an internal cache regeneration request. + * + * @param string $url URL being regenerated. + * + * @return string + */ + private function create_regen_token( $url ) { + return md5( $url . '|' . microtime( true ) . '|' . wp_rand( 1, PHP_INT_MAX ) ); + } + /** * Confirm a URL belongs to the current site before internal HTTP warmups. * @@ -989,7 +1008,18 @@ private function get_cache_key_for_url( $normalized_url ) { * @return bool */ private function is_internal_regen_request() { - return isset( $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] ) && '1' === sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] ) ); + if ( '' === $this->lock_key || empty( $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] ) ) { + return false; + } + + $expected_token = get_transient( $this->lock_key ); + if ( ! is_string( $expected_token ) || '' === $expected_token ) { + return false; + } + + $provided_token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] ) ); + + return hash_equals( $expected_token, $provided_token ); } /** @@ -1017,7 +1047,7 @@ private function acquire_lock() { return false; } - return set_transient( $this->lock_key, 1, 30 ); + return set_transient( $this->lock_key, $this->create_regen_token( $this->current_url ), 30 ); } /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index e3ecaad..e0d9838 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,11 +19,41 @@ function update_option( $name, $value, $autoload = null ) { if ( ! isset( $GLOBALS['perform_test_options'] ) || ! is_array( $GLOBALS['perform_test_options'] ) ) { $GLOBALS['perform_test_options'] = []; } + + if ( array_key_exists( $name, $GLOBALS['perform_test_options'] ) && $GLOBALS['perform_test_options'][ $name ] === $value ) { + return false; + } + $GLOBALS['perform_test_options'][ $name ] = $value; return true; } } +if ( ! function_exists( 'get_transient' ) ) { + function get_transient( $transient ) { + $store = isset( $GLOBALS['perform_test_transients'] ) && is_array( $GLOBALS['perform_test_transients'] ) ? $GLOBALS['perform_test_transients'] : []; + return array_key_exists( $transient, $store ) ? $store[ $transient ] : false; + } +} + +if ( ! function_exists( 'set_transient' ) ) { + function set_transient( $transient, $value, $expiration = 0 ) { + if ( ! isset( $GLOBALS['perform_test_transients'] ) || ! is_array( $GLOBALS['perform_test_transients'] ) ) { + $GLOBALS['perform_test_transients'] = []; + } + + $GLOBALS['perform_test_transients'][ $transient ] = $value; + return true; + } +} + +if ( ! function_exists( 'delete_transient' ) ) { + function delete_transient( $transient ) { + unset( $GLOBALS['perform_test_transients'][ $transient ] ); + return true; + } +} + if ( ! function_exists( 'add_action' ) ) { function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { return true; @@ -42,6 +72,78 @@ function wp_enqueue_script( $handle, $src = '', $deps = [], $ver = false, $in_fo } } +if ( ! function_exists( 'add_query_arg' ) ) { + function add_query_arg( $key, $value = null, $url = null ) { + $url = null === $url ? ( $GLOBALS['perform_test_current_url'] ?? 'https://example.com/' ) : $url; + $args = is_array( $key ) ? $key : [ $key => $value ]; + + $parts = wp_parse_url( $url ); + $query = []; + if ( ! empty( $parts['query'] ) ) { + parse_str( $parts['query'], $query ); + } + + foreach ( $args as $arg_key => $arg_value ) { + $query[ $arg_key ] = $arg_value; + } + + $scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : ''; + $host = $parts['host'] ?? ''; + $path = $parts['path'] ?? ''; + $result = $scheme . $host . $path; + $query = array_filter( + $query, + static function ( $arg_value ) { + return null !== $arg_value && false !== $arg_value; + } + ); + + return empty( $query ) ? $result : $result . '?' . http_build_query( $query ); + } +} + +if ( ! function_exists( 'remove_query_arg' ) ) { + function remove_query_arg( $key, $url = null ) { + $url = null === $url ? ( $GLOBALS['perform_test_current_url'] ?? 'https://example.com/' ) : $url; + $keys = (array) $key; + + $parts = wp_parse_url( $url ); + $query = []; + if ( ! empty( $parts['query'] ) ) { + parse_str( $parts['query'], $query ); + } + + foreach ( $keys as $arg_key ) { + unset( $query[ $arg_key ] ); + } + + $scheme = isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : ''; + $host = $parts['host'] ?? ''; + $path = $parts['path'] ?? ''; + $result = $scheme . $host . $path; + + return empty( $query ) ? $result : $result . '?' . http_build_query( $query ); + } +} + +if ( ! function_exists( 'admin_url' ) ) { + function admin_url( $path = '' ) { + return 'https://example.com/wp-admin/' . ltrim( $path, '/' ); + } +} + +if ( ! function_exists( 'esc_url' ) ) { + function esc_url( $url ) { + return (string) $url; + } +} + +if ( ! function_exists( 'esc_html__' ) ) { + function esc_html__( $text, $domain = 'default' ) { + return (string) $text; + } +} + if ( ! function_exists( 'current_user_can' ) ) { function current_user_can( $capability ) { $capabilities = isset( $GLOBALS['perform_test_current_user_can'] ) && is_array( $GLOBALS['perform_test_current_user_can'] ) ? $GLOBALS['perform_test_current_user_can'] : []; diff --git a/tests/unit-tests/tests-admin-actions.php b/tests/unit-tests/tests-admin-actions.php new file mode 100644 index 0000000..69a7e0d --- /dev/null +++ b/tests/unit-tests/tests-admin-actions.php @@ -0,0 +1,77 @@ + true, + ]; + $GLOBALS['perform_test_is_admin'] = false; + $GLOBALS['perform_test_current_url'] = 'https://example.com/current-page/?foo=bar'; + unset( $_GET['perform'] ); + } + + protected function tearDown(): void { + unset( + $_GET['perform'], + $GLOBALS['perform_test_current_user_can'], + $GLOBALS['perform_test_is_admin'], + $GLOBALS['perform_test_current_url'] + ); + } + + public function test_admin_bar_assets_manager_link_adds_perform_query_arg() { + $admin_bar = new Perform_Test_Admin_Bar(); + $actions = new AdminActions(); + + $actions->add_to_admin_bar( $admin_bar ); + + $this->assertSame( 'https://example.com/current-page/?foo=bar&perform=1', $admin_bar->get_menu_href( 'assets-manager' ) ); + } + + public function test_admin_bar_assets_manager_close_link_removes_perform_query_arg() { + $_GET['perform'] = '1'; + $GLOBALS['perform_test_current_url'] = 'https://example.com/current-page/?foo=bar&perform=1'; + + $admin_bar = new Perform_Test_Admin_Bar(); + $actions = new AdminActions(); + + $actions->add_to_admin_bar( $admin_bar ); + + $this->assertSame( 'https://example.com/current-page/?foo=bar', $admin_bar->get_menu_href( 'assets-manager' ) ); + } +} + +// phpcs:ignore Generic.Files.OneObjectStructurePerFile.MultipleFound -- Small test fixture for the admin bar API. +final class Perform_Test_Admin_Bar { + /** + * Captured admin bar menu items. + * + * @var array> + */ + private $menus = []; + + /** + * Capture an admin bar menu registration. + * + * @param array $args Menu args. + * + * @return void + */ + public function add_menu( $args ) { + $this->menus[ $args['id'] ] = $args; + } + + /** + * Get captured menu href by ID. + * + * @param string $id Menu ID. + * + * @return string + */ + public function get_menu_href( $id ) { + return isset( $this->menus[ $id ]['href'] ) ? (string) $this->menus[ $id ]['href'] : ''; + } +} diff --git a/tests/unit-tests/tests-basic-modules.php b/tests/unit-tests/tests-basic-modules.php new file mode 100644 index 0000000..420d1e0 --- /dev/null +++ b/tests/unit-tests/tests-basic-modules.php @@ -0,0 +1,28 @@ + 'https://example.com', + ]; + } + + protected function tearDown(): void { + unset( $GLOBALS['perform_test_options'] ); + } + + public function test_disable_self_pingbacks_removes_home_links_by_reference() { + $links = [ + 'https://example.com/internal-post', + 'https://external.example/post', + ]; + + $module = new DisableSelfPingbacks(); + $module->disable_self_pingbacks( $links ); + + $this->assertSame( [ 1 => 'https://external.example/post' ], $links ); + } +} diff --git a/tests/unit-tests/tests-page-cache.php b/tests/unit-tests/tests-page-cache.php new file mode 100644 index 0000000..f7b136f --- /dev/null +++ b/tests/unit-tests/tests-page-cache.php @@ -0,0 +1,34 @@ + 'expected-token', + ]; + unset( $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] ); + } + + protected function tearDown(): void { + unset( $GLOBALS['perform_test_transients'], $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] ); + } + + public function test_internal_regeneration_requires_matching_lock_token() { + $page_cache = new PageCache(); + + $lock_key = new ReflectionProperty( $page_cache, 'lock_key' ); + $lock_key->setAccessible( true ); + $lock_key->setValue( $page_cache, 'perform_cache_lock_test' ); + + $is_internal_regen_request = new ReflectionMethod( $page_cache, 'is_internal_regen_request' ); + $is_internal_regen_request->setAccessible( true ); + + $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] = '1'; + $this->assertFalse( $is_internal_regen_request->invoke( $page_cache ) ); + + $_SERVER['HTTP_X_PERFORM_CACHE_REGEN'] = 'expected-token'; + $this->assertTrue( $is_internal_regen_request->invoke( $page_cache ) ); + } +}