PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 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 All 35 releases
← All changes | includes/class-settings-manager.php +49 -0 1.3.3 → 1.4.1 View file →
@@ -1086,8 +1086,26 @@
1086 1086 if ( self::is_secret_field( $key, $spec ) && array_key_exists( $key, $settings ) ) {
1087 1087 $settings[ $key ] = self::mask_secret_value( (string) $settings[ $key ] );
1088 1088 }
1089 1089 }
1090 +
1091 + /*
1092 + * Preserved keys are carried through by `get()` and have no schema, so
1093 + * this loop never saw them — a module preserving a credential would
1094 + * print it here in full. The MCP module preserves its pairing token,
1095 + * which is what made that concrete.
1096 + *
1097 + * Matched by NAME, since a key with no spec has no declared type. A
1098 + * scalar only: `mask_secret_value` takes a string, and a preserved key
1099 + * can hold an array (the REST-cache `rules`), which is not a secret
1100 + * and must not be flattened into one.
1101 + */
1102 + foreach ( $module->preserved_keys() as $key ) {
1103 + if ( array_key_exists( $key, $settings ) && is_scalar( $settings[ $key ] ) && self::is_secret_key( $key ) ) {
1104 + $settings[ $key ] = self::mask_secret_value( (string) $settings[ $key ] );
1105 + }
1106 + }
1107 +
1090 1108 return $settings;
1091 1109 }
1092 1110
1093 1111 /**
@@ -1746,8 +1764,10 @@
1746 1764 $u = esc_url_raw( (string) $item );
1747 1765 if ( $u ) {
1748 1766 $out[] = $u;
1749 1767 }
1768 + } elseif ( 'path' === $item_type ) {
1769 + $out[] = self::sanitize_path_pattern( (string) $item );
1750 1770 } else {
1751 1771 $out[] = sanitize_text_field( (string) $item );
1752 1772 }
1753 1773 }
@@ -1820,8 +1840,37 @@
1820 1840 if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
1821 1841 return false;
1822 1842 }
1823 1843 return true;
1844 + }
1845 +
1846 + /**
1847 + * sanitize_text_field() for a URL path pattern, minus one step: it keeps
1848 + * percent-encoded octets.
1849 + *
1850 + * sanitize_text_field() deletes every `%XX`, so an excluded URL pasted as
1851 + * `/%e7%ac%ac%e5%8d%81/` (how WordPress spells a Chinese slug) was saved
1852 + * as `//`, and `/%e8%b3%bc%e7%89%a9%e8%bb%8a` as `/`, which "contains"-
1853 + * matches every page and turned the page cache off for the whole site.
1854 + * The `%` is swapped for a private-use character while the rest of
1855 + * sanitize_text_field() runs (tags, line breaks, invalid UTF-8), then
1856 + * swapped back.
1857 + *
1858 + * Escapes are stored in lower case, the spelling the page cache matches
1859 + * paths in. An entry copied from Chrome's address bar arrives as
1860 + * `/%E8%81%AF…/`; stored as typed, it never matched.
1861 + */
1862 + private static function sanitize_path_pattern( string $value ): string {
1863 + $mark = "\u{E000}";
1864 + $value = str_replace( $mark, '', $value );
1865 + $value = str_replace( $mark, '%', sanitize_text_field( str_replace( '%', $mark, $value ) ) );
1866 + return (string) preg_replace_callback(
1867 + '/%[0-9a-fA-F]{2}/',
1868 + static function ( array $m ): string {
1869 + return strtolower( $m[0] );
1870 + },
1871 + $value
1872 + );
1824 1873 }
1825 1874
1826 1875 /**
1827 1876 * Whether a URL looks like an image — used to gate `media` fields so a