| @@ -172,9 +172,19 @@ | ||
| 172 | 172 | * @param mixed $actual Value of the gating constant, or null. |
| 173 | 173 | * @param string $on Gating constant name. |
| 174 | 174 | */ |
| 175 | 175 | $actual = apply_filters( 'xspeed_constant_gate_value', $actual, $on ); |
| 176 | - if ( $actual !== ( $gate['is'] ?? null ) ) { | |
| 176 | + $want = $gate['is'] ?? null; | |
| 177 | + /* | |
| 178 | + * `case_insensitive` is for gates on another plugin's | |
| 179 | + * convention that is read that way at its source: Redis | |
| 180 | + * Object Cache matches WP_REDIS_SCHEME with strcasecmp(), so | |
| 181 | + * `UNIX` is a socket there and has to be one here. (#566) | |
| 182 | + */ | |
| 183 | + $match = ! empty( $gate['case_insensitive'] ) && is_string( $actual ) && is_string( $want ) | |
| 184 | + ? 0 === strcasecmp( $actual, $want ) | |
| 185 | + : $actual === $want; | |
| 186 | + if ( ! $match ) { | |
| 177 | 187 | continue; |
| 178 | 188 | } |
| 179 | 189 | } |
| 180 | 190 | return $name; |
| @@ -1086,8 +1096,26 @@ | ||
| 1086 | 1096 | if ( self::is_secret_field( $key, $spec ) && array_key_exists( $key, $settings ) ) { |
| 1087 | 1097 | $settings[ $key ] = self::mask_secret_value( (string) $settings[ $key ] ); |
| 1088 | 1098 | } |
| 1089 | 1099 | } |
| 1100 | + | |
| 1101 | + /* | |
| 1102 | + * Preserved keys are carried through by `get()` and have no schema, so | |
| 1103 | + * this loop never saw them — a module preserving a credential would | |
| 1104 | + * print it here in full. The MCP module preserves its pairing token, | |
| 1105 | + * which is what made that concrete. | |
| 1106 | + * | |
| 1107 | + * Matched by NAME, since a key with no spec has no declared type. A | |
| 1108 | + * scalar only: `mask_secret_value` takes a string, and a preserved key | |
| 1109 | + * can hold an array (the REST-cache `rules`), which is not a secret | |
| 1110 | + * and must not be flattened into one. | |
| 1111 | + */ | |
| 1112 | + foreach ( $module->preserved_keys() as $key ) { | |
| 1113 | + if ( array_key_exists( $key, $settings ) && is_scalar( $settings[ $key ] ) && self::is_secret_key( $key ) ) { | |
| 1114 | + $settings[ $key ] = self::mask_secret_value( (string) $settings[ $key ] ); | |
| 1115 | + } | |
| 1116 | + } | |
| 1117 | + | |
| 1090 | 1118 | return $settings; |
| 1091 | 1119 | } |
| 1092 | 1120 | |
| 1093 | 1121 | /** |
| @@ -1746,8 +1774,10 @@ | ||
| 1746 | 1774 | $u = esc_url_raw( (string) $item ); |
| 1747 | 1775 | if ( $u ) { |
| 1748 | 1776 | $out[] = $u; |
| 1749 | 1777 | } |
| 1778 | + } elseif ( 'path' === $item_type ) { | |
| 1779 | + $out[] = self::sanitize_path_pattern( (string) $item ); | |
| 1750 | 1780 | } else { |
| 1751 | 1781 | $out[] = sanitize_text_field( (string) $item ); |
| 1752 | 1782 | } |
| 1753 | 1783 | } |
| @@ -1820,8 +1850,37 @@ | ||
| 1820 | 1850 | if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) { |
| 1821 | 1851 | return false; |
| 1822 | 1852 | } |
| 1823 | 1853 | return true; |
| 1854 | + } | |
| 1855 | + | |
| 1856 | + /** | |
| 1857 | + * sanitize_text_field() for a URL path pattern, minus one step: it keeps | |
| 1858 | + * percent-encoded octets. | |
| 1859 | + * | |
| 1860 | + * sanitize_text_field() deletes every `%XX`, so an excluded URL pasted as | |
| 1861 | + * `/%e7%ac%ac%e5%8d%81/` (how WordPress spells a Chinese slug) was saved | |
| 1862 | + * as `//`, and `/%e8%b3%bc%e7%89%a9%e8%bb%8a` as `/`, which "contains"- | |
| 1863 | + * matches every page and turned the page cache off for the whole site. | |
| 1864 | + * The `%` is swapped for a private-use character while the rest of | |
| 1865 | + * sanitize_text_field() runs (tags, line breaks, invalid UTF-8), then | |
| 1866 | + * swapped back. | |
| 1867 | + * | |
| 1868 | + * Escapes are stored in lower case, the spelling the page cache matches | |
| 1869 | + * paths in. An entry copied from Chrome's address bar arrives as | |
| 1870 | + * `/%E8%81%AF…/`; stored as typed, it never matched. | |
| 1871 | + */ | |
| 1872 | + private static function sanitize_path_pattern( string $value ): string { | |
| 1873 | + $mark = "\u{E000}"; | |
| 1874 | + $value = str_replace( $mark, '', $value ); | |
| 1875 | + $value = str_replace( $mark, '%', sanitize_text_field( str_replace( '%', $mark, $value ) ) ); | |
| 1876 | + return (string) preg_replace_callback( | |
| 1877 | + '/%[0-9a-fA-F]{2}/', | |
| 1878 | + static function ( array $m ): string { | |
| 1879 | + return strtolower( $m[0] ); | |
| 1880 | + }, | |
| 1881 | + $value | |
| 1882 | + ); | |
| 1824 | 1883 | } |
| 1825 | 1884 | |
| 1826 | 1885 | /** |
| 1827 | 1886 | * Whether a URL looks like an image — used to gate `media` fields so a |