PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.7
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.7
1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 All 33 releases
← All changes | includes/class-cache.php +170 -7 1.3.6 → 1.3.7 View file →
@@ -257,9 +257,26 @@
257 257 // alone cannot tell "added beside nothing" from "replaced live code".
258 258 // This filter fires only when the upgrader removed an existing copy,
259 259 // which is exactly the difference. Registered as a filter listener
260 260 // that returns its input untouched. (#303)
261 - add_filter( 'upgrader_clear_destination', array( __CLASS__, 'note_cleared_destination' ), 10, 1 );
261 + add_filter( 'upgrader_clear_destination', array( __CLASS__, 'note_cleared_destination' ), 10, 4 );
262 + // …but `upgrader_clear_destination` fires whenever the upgrader was
263 + // ASKED to clear, not only when it removed something:
264 + // WP_Upgrader::clear_destination() returns true early when the
265 + // destination does not exist. Looked at before the delete, while the
266 + // old copy is still on disk. (#303)
267 + //
268 + // PHP_INT_MAX, because the folder name is only final once every other
269 + // listener has had its turn. Update libraries that normalise
270 + // `plugin-1.2.3/` to `plugin/` (Plugin Update Checker, EDD Software
271 + // Licensing, GitHub-sourced zips) rename the extracted directory on
272 + // this same filter at priority 10 or later, and core derives the real
273 + // destination from the FILTERED source. Measured at 10, a genuine
274 + // replacement read as "nothing was there" and the stale cache stayed.
275 + // note_cleared_destination() cross-checks the folder core actually
276 + // cleared against the one measured here, for a renamer that runs
277 + // later still. (#407 QA)
278 + add_filter( 'upgrader_source_selection', array( __CLASS__, 'note_destination_state' ), PHP_INT_MAX, 4 );
262 279 // Unattended auto-updates are the case that matters most here: they
263 280 // land overnight with nobody around to purge by hand, which is the
264 281 // exact scenario the stale cache goes undiagnosed in. WordPress fires
265 282 // this INSTEAD of a per-item upgrader_process_complete for some
@@ -1209,9 +1226,19 @@
1209 1226 $key = self::cache_key();
1210 1227 $file = self::cache_file_for( $key );
1211 1228
1212 1229 if ( file_exists( $file ) && ! self::is_expired( $file ) ) {
1213 - Hit_Counter::record_hit();
1230 + // Symmetric with the miss branch below: a bot, scanner or one of
1231 + // xSpeed's own warm/benchmark requests that lands a HIT must not
1232 + // inflate the ratio either — excluding only their misses would
1233 + // shrink the denominator while their hits kept feeding the
1234 + // numerator, making the displayed ratio MORE optimistic than
1235 + // before the exclusion existed.
1236 + if ( self::miss_is_excluded() ) {
1237 + Hit_Counter::record_excluded();
1238 + } else {
1239 + Hit_Counter::record_hit();
1240 + }
1214 1241 // Emit the HIT marker on THIS path too. The drop-in
1215 1242 // (advanced-cache.php) sends "HIT (php)" and the nginx static
1216 1243 // rewrite sends "HIT (nginx)", but this template_redirect
1217 1244 // serve path — the one that runs when the drop-in isn't loaded
@@ -1824,11 +1851,27 @@
1824 1851 * `~utm_…` default vs `my_utm_source`. A param name that is genuinely
1825 1852 * unknown now bypasses the cache, which is the safe direction.
1826 1853 */
1827 1854 private static function query_key_is_ignored( string $key, array $ignored ): bool {
1855 + if ( in_array( $key, self::NEVER_IGNORED_QUERY_PARAMS, true ) ) {
1856 + return false;
1857 + }
1828 1858 return Glob_Matcher::any_match_name( $ignored, $key );
1829 1859 }
1830 1860
1861 + /**
1862 + * Query params no ignored-params entry can match, glob or regex.
1863 + *
1864 + * A measuring request asks for the page as it is before optimisation
1865 + * (`xspeed_css=off`), with a one-time value (`xspeed_nc`) so no cache
1866 + * has a copy of it. The answer must be rendered for that request. A
1867 + * list entry such as `xspeed_*` or `*` would make both params
1868 + * decoration: the drop-in would serve the canonical, already-optimised
1869 + * entry before any plugin loads, and the measurement would describe
1870 + * the optimised page instead of the source.
1871 + */
1872 + public const NEVER_IGNORED_QUERY_PARAMS = array( 'xspeed_css', 'xspeed_nc' );
1873 +
1831 1874 public static function cache_key() {
1832 1875 $host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : 'default';
1833 1876
1834 1877 // Cacheable 404s share ONE generic per-host entry — keying them by
@@ -4679,8 +4722,13 @@
4679 4722 * more than it needs to. A cold cache is the cheap direction, and the
4680 4723 * alternative — scoping the signal per upgrader — is not knowable from
4681 4724 * `upgrader_clear_destination`.
4682 4725 *
4726 + * The observed-destination signal is dropped here too. The two are read
4727 + * together and have to expire together: leaving "the directory was not
4728 + * there" behind would let a first install answer for whatever ran next
4729 + * in the same request, and that one's mistake is a cache left stale.
4730 + *
4683 4731 * @return void
4684 4732 */
4685 4733 public static function forget_cleared_destination(): void {
4686 4734 if ( self::$upgrade_dispatch_depth > 0 ) {
@@ -4688,12 +4736,84 @@
4688 4736 }
4689 4737
4690 4738 if ( 0 === self::$upgrade_dispatch_depth ) {
4691 4739 self::$upgrade_cleared_destination = false;
4740 + self::$upgrade_destination_existed = null;
4741 + self::$upgrade_destination_folder = '';
4692 4742 }
4693 4743 }
4694 4744
4695 4745 /**
4746 + * Whether the destination this run installs into was already there.
4747 + *
4748 + * Null while unknown — an upgrader whose target we cannot work out keeps
4749 + * the old behaviour rather than being guessed at.
4750 + *
4751 + * @var bool|null
4752 + */
4753 + private static $upgrade_destination_existed = null;
4754 +
4755 + /**
4756 + * The folder name that answer was measured against, so the destination
4757 + * WordPress reports on `upgrader_clear_destination` can be checked
4758 + * against it. Empty when nothing was measured.
4759 + *
4760 + * @var string
4761 + */
4762 + private static $upgrade_destination_folder = '';
4763 +
4764 + /**
4765 + * Note whether the package's destination exists, before it is cleared.
4766 + *
4767 + * `upgrader_source_selection` is the last hook that fires while the old
4768 + * copy is still on disk, and the extracted source folder name is the
4769 + * directory the package will install into. A pass-through listener: the
4770 + * source is returned untouched.
4771 + *
4772 + * @param mixed $source Extracted package directory.
4773 + * @param mixed $remote_src Unused; the package's remote source.
4774 + * @param mixed $upgrader The upgrader instance, if one was supplied.
4775 + * @param mixed $hook_extra Context supplied by the upgrader.
4776 + * @return mixed The source, unchanged.
4777 + */
4778 + public static function note_destination_state( $source, $remote_src = '', $upgrader = null, $hook_extra = array() ) {
4779 + self::$upgrade_destination_existed = null;
4780 + self::$upgrade_destination_folder = '';
4781 +
4782 + $root = self::upgrade_destination_root( $upgrader, is_array( $hook_extra ) ? $hook_extra : array() );
4783 + if ( null !== $root && is_string( $source ) && '' !== $source ) {
4784 + $folder = basename( rtrim( $source, '/\\' ) );
4785 + if ( '' !== $folder ) {
4786 + self::$upgrade_destination_existed = is_dir( rtrim( $root, '/\\' ) . '/' . $folder );
4787 + self::$upgrade_destination_folder = $folder;
4788 + }
4789 + }
4790 +
4791 + return $source;
4792 + }
4793 +
4794 + /**
4795 + * Where a package of this kind installs to, or null if we cannot tell.
4796 + *
4797 + * @param mixed $upgrader The upgrader instance, if one was supplied.
4798 + * @param array $hook_extra Context supplied by the upgrader.
4799 + * @return string|null
4800 + */
4801 + private static function upgrade_destination_root( $upgrader, array $hook_extra ): ?string {
4802 + $type = isset( $hook_extra['type'] ) ? (string) $hook_extra['type'] : '';
4803 +
4804 + if ( 'plugin' === $type || $upgrader instanceof \Plugin_Upgrader ) {
4805 + return defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : null;
4806 + }
4807 +
4808 + if ( 'theme' === $type || $upgrader instanceof \Theme_Upgrader ) {
4809 + return function_exists( 'get_theme_root' ) ? (string) get_theme_root() : null;
4810 + }
4811 +
4812 + return null;
4813 + }
4814 +
4815 + /**
4696 4816 * Record that the upgrader cleared an existing destination.
4697 4817 *
4698 4818 * A pass-through listener on `upgrader_clear_destination`: WordPress only
4699 4819 * fires it when `clear_destination` was set AND something was there to
@@ -4702,13 +4822,28 @@
4702 4822 *
4703 4823 * @param true|\WP_Error $removed Whether the destination was cleared.
4704 4824 * @return true|\WP_Error
4705 4825 */
4706 - public static function note_cleared_destination( $removed ) {
4826 + public static function note_cleared_destination( $removed, $local_destination = '', $remote_destination = '', $hook_extra = array() ) {
4707 4827 if ( ! is_wp_error( $removed ) ) {
4708 4828 self::$upgrade_cleared_destination = true;
4709 4829 }
4710 4830
4831 + // $remote_destination is the directory WordPress actually cleared,
4832 + // derived from the source AFTER every `upgrader_source_selection`
4833 + // listener ran. If its folder is not the one note_destination_state()
4834 + // measured, a listener that ran after ours renamed the package, and
4835 + // the "was it there?" answer is about a directory that was never going
4836 + // to be written. Unknown is the answer that purges, so that is what it
4837 + // becomes. Only the last segment is compared: over FTP the remote
4838 + // path sits under the server's own root, not WP_PLUGIN_DIR. (#407 QA)
4839 + if ( is_string( $remote_destination ) && '' !== $remote_destination ) {
4840 + $cleared_folder = basename( rtrim( $remote_destination, '/\\' ) );
4841 + if ( '' !== $cleared_folder && $cleared_folder !== self::$upgrade_destination_folder ) {
4842 + self::$upgrade_destination_existed = null;
4843 + }
4844 + }
4845 +
4711 4846 return $removed;
4712 4847 }
4713 4848
4714 4849 /**
@@ -4822,8 +4957,18 @@
4822 4957 if ( 'install' === $action && ! $cleared ) {
4823 4958 return false;
4824 4959 }
4825 4960
4961 + // A cleared destination is only evidence of a replacement if there was
4962 + // something in it. Core returns success from clear_destination() for a
4963 + // destination that never existed, so a first-time install arrived here
4964 + // looking exactly like an upload-and-replace and bought a cold cache
4965 + // for a plugin that is not even active yet. Only acted on when we
4966 + // positively know the directory was absent.
4967 + if ( 'install' === $action && false === self::$upgrade_destination_existed ) {
4968 + return false;
4969 + }
4970 +
4826 4971 // 'translation' is the one update type that cannot change rendered
4827 4972 // markup. Anything else — including an empty type from a custom
4828 4973 // updater — is treated as cache-invalidating, because guessing wrong
4829 4974 // in that direction only costs a cold cache.
@@ -5648,8 +5793,13 @@
5648 5793 private static function miss_is_excluded(): bool {
5649 5794 if ( function_exists( 'is_404' ) && is_404() ) {
5650 5795 return true;
5651 5796 }
5797 + // A marked request is ours whatever its UA says: a renamed warmer,
5798 + // or a probe that has to send a browser's UA.
5799 + if ( Self_Traffic::request_is_marked() ) {
5800 + return true;
5801 + }
5652 5802 $ua = isset( $_SERVER['HTTP_USER_AGENT'] )
5653 5803 ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) )
5654 5804 : '';
5655 5805 return Hit_Counter::is_bot_ua( $ua );
@@ -6420,9 +6570,9 @@
6420 6570 */
6421 6571 $opts = self::stored_cache_opts();
6422 6572 $ignored = is_array( $opts['ignored_query_params'] ?? null )
6423 6573 ? $opts['ignored_query_params']
6424 - : \XSpeed\Modules\Cache\CacheModule::DEFAULT_IGNORED_QUERY_PARAMS;
6574 + : \XSpeed\Modules\Cache\CacheModule::default_ignored_query_params();
6425 6575
6426 6576 $parts = array();
6427 6577 foreach ( $ignored as $pattern ) {
6428 6578 $pattern = trim( (string) $pattern );
@@ -6456,9 +6606,12 @@
6456 6606 if ( ! is_dir( XSPEED_CACHE_DIR ) && ! wp_mkdir_p( XSPEED_CACHE_DIR ) ) {
6457 6607 return;
6458 6608 }
6459 6609
6460 - $payload = '(?:' . implode( '|', array_unique( $parts ) ) . ')';
6610 + // The drop-in anchors this as `^…$`, so the lookahead refuses the
6611 + // never-ignored names whole, whatever entry would have matched them.
6612 + $never = implode( '|', array_map( static fn ( $p ) => preg_quote( $p, '#' ), self::NEVER_IGNORED_QUERY_PARAMS ) );
6613 + $payload = '(?!(?:' . $never . ')$)(?:' . implode( '|', array_unique( $parts ) ) . ')';
6461 6614
6462 6615 // Only write when the value actually changed. This runs from
6463 6616 // reconcile_mobile_separate() on CacheModule::boot(), so an
6464 6617 // unconditional write cost a file write and an exclusive lock on every
@@ -6972,9 +7125,11 @@
6972 7125 'redirection' => 2,
6973 7126 // Bust any per-device cache so we compare freshly-rendered
6974 7127 // HTML, and pass the device UA the site would branch on.
6975 7128 'user-agent' => $ua,
6976 - 'headers' => array( 'Cache-Control' => 'no-cache' ),
7129 + // A real device UA by design, so only the header marks
7130 + // this as ours to analytics and the hit ratio.
7131 + 'headers' => Self_Traffic::headers( array( 'Cache-Control' => 'no-cache' ) ),
6977 7132 )
6978 7133 );
6979 7134 if ( is_wp_error( $resp ) || 200 !== (int) wp_remote_retrieve_response_code( $resp ) ) {
6980 7135 return null;
@@ -7794,9 +7949,9 @@
7794 7949 // don't repeat the wait every minute.
7795 7950 'timeout' => 3,
7796 7951 'sslverify' => ! $is_local,
7797 7952 'redirection' => 0,
7798 - 'headers' => array( 'Cache-Control' => 'no-cache' ),
7953 + 'headers' => Self_Traffic::headers( array( 'Cache-Control' => 'no-cache' ) ),
7799 7954 )
7800 7955 );
7801 7956
7802 7957 // Best-effort cleanup so we don't accumulate probe dirs even
@@ -8516,8 +8671,16 @@
8516 8671 );
8517 8672 $source_contents = str_replace(
8518 8673 '@@XSPEED_UA_RE@@',
8519 8674 str_replace( "'", "\\'", $ua_rule['regex'] ),
8675 + $source_contents
8676 + );
8677 + // Which user agents must not count toward the hit ratio. Built here
8678 + // because `xspeed_self_user_agents` is a filter the drop-in cannot
8679 + // call. A renamed warmer is caught by Self_Traffic::HEADER instead.
8680 + $source_contents = str_replace(
8681 + '@@XSPEED_HIT_EXCLUDE_RE@@',
8682 + str_replace( "'", "\\'", Hit_Counter::excluded_ua_regex() ),
8520 8683 $source_contents
8521 8684 );
8522 8685
8523 8686 /*