Changeset 3743505 for optimazz/trunk/includes/Modules/Cache/engine.php
- Timestamp:
- 10/13/2026 10:00:51 AM (13 days ago)
- File:
-
- 1 edited
-
optimazz/trunk/includes/Modules/Cache/engine.php (modified) (15 diffs)
Legend:
- Unmodified
- Added
- Removed
-
optimazz/trunk/includes/Modules/Cache/engine.php
r3723128 r3743505 38 38 'site_host' => '', 39 39 'lifespan_hours' => 0, 40 // Multisite: map of "host/path/" => site ID for every site Optimazz 41 // caches, and the site the current request belongs to. 42 'multisite' => false, 43 'sites' => array(), 44 'site_id' => 0, 40 45 ); 41 46 } … … 77 82 78 83 /** 84 * Config for a request to $host + $uri. On a network this resolves the 85 * site (by host and the longest matching path) and merges that site's own 86 * rules — exclusions and ignored query args — over the network settings. 87 * A request for a host/path that no cached site owns gets enabled=false. 88 * 89 * @param string $host Host header. 90 * @param string $uri Request URI. 91 * @return array 92 */ 93 public static function config_for( $host, $uri ) { 94 $config = self::load_config(); 95 if ( empty( $config['multisite'] ) ) { 96 return $config; 97 } 98 $id = self::resolve_site( $config, $host, $uri ); 99 if ( ! $id ) { 100 $config['enabled'] = false; 101 return $config; 102 } 103 $config['site_id'] = $id; 104 $file = self::site_rules_file( $id ); 105 if ( is_readable( $file ) ) { 106 $rules = include $file; 107 if ( is_array( $rules ) ) { 108 foreach ( array( 'ignored_query_args', 'excluded_uris', 'excluded_cookies', 'excluded_user_agents' ) as $key ) { 109 if ( isset( $rules[ $key ] ) && is_array( $rules[ $key ] ) ) { 110 $config[ $key ] = $rules[ $key ]; 111 } 112 } 113 } 114 } 115 return $config; 116 } 117 118 /** 119 * Which network site a host + URI belongs to: the site whose "host/path/" 120 * key is the longest prefix of the request (so /shop/… goes to the /shop/ 121 * site, everything else on that host to the main site). 122 * 123 * @param array $config Config with a 'sites' map. 124 * @param string $host Host header. 125 * @param string $uri Request URI. 126 * @return int Site ID, or 0 when no cached site matches. 127 */ 128 public static function resolve_site( array $config, $host, $uri ) { 129 // Host as sent (a port is kept, matching how the cache folder is named 130 // and how WordPress stores a site's domain). 131 $host = strtolower( (string) $host ); 132 $path = (string) strtok( (string) $uri, '?' ); 133 $path = '/' . ltrim( $path, '/' ); 134 $key = $host . rtrim( $path, '/' ) . '/'; 135 $best = 0; 136 $len = 0; 137 foreach ( (array) $config['sites'] as $prefix => $id ) { 138 $prefix = (string) $prefix; 139 if ( strlen( $prefix ) > $len && 0 === strpos( $key, $prefix ) ) { 140 $best = (int) $id; 141 $len = strlen( $prefix ); 142 } 143 } 144 return $best; 145 } 146 147 /** 148 * Host of a parsed URL as a browser sends it in the Host header — with the 149 * port when there is one. Cache folders and the network site map are keyed 150 * on that, so URL-based lookups (purge, preload) must use it too. 151 * 152 * @param array $parts parse_url() result. 153 * @return string 154 */ 155 public static function url_host( array $parts ) { 156 $host = isset( $parts['host'] ) ? strtolower( (string) $parts['host'] ) : ''; 157 return ( '' !== $host && ! empty( $parts['port'] ) ) ? $host . ':' . (int) $parts['port'] : $host; 158 } 159 160 /** 161 * File holding one network site's own cache rules. 162 * 163 * @param int $id Site ID. 164 * @return string 165 */ 166 public static function site_rules_file( $id ) { 167 return self::cache_root() . 'sites/' . (int) $id . '.php'; 168 } 169 170 /** 171 * Write one network site's own cache rules (only changes when they differ). 172 * 173 * @param int $id Site ID. 174 * @param array $rules Rules: ignored_query_args, excluded_uris, excluded_cookies, excluded_user_agents. 175 * @return bool 176 */ 177 public static function write_site_rules( $id, array $rules ) { 178 $file = self::site_rules_file( $id ); 179 // var_export serialises to a fast include file, not for debugging. 180 $code = "<?php\n// Generated by Optimazz. Do not edit.\nreturn " . var_export( $rules, true ) . ";\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export 181 if ( is_readable( $file ) && (string) @file_get_contents( $file ) === $code ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions 182 return true; 183 } 184 self::mkdir( dirname( $file ) ); 185 self::protect_dir( self::cache_root() ); 186 return self::atomic_write( $file, $code ); 187 } 188 189 /** 79 190 * Persist config to disk as a returnable PHP array (fast to include). 80 191 * … … 87 198 // var_export is used to serialize the config to a fast include file, not for debugging. 88 199 $code = "<?php\n// Generated by Optimazz. Do not edit.\nreturn " . var_export( $config, true ) . ";\n"; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export 200 if ( is_readable( self::config_file() ) && (string) @file_get_contents( self::config_file() ) === $code ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions 201 return true; // Unchanged: keep the compiled copy (no opcode-cache churn). 202 } 89 203 return self::atomic_write( self::config_file(), $code ); 90 204 } … … 144 258 */ 145 259 public static function serve() { 146 $config = self::load_config(); 260 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- pre-WP drop-in; only used to pick a site and build a sanitized cache key. 261 $config = self::config_for( isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '', isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '/' ); 147 262 148 263 if ( empty( $config['enabled'] ) ) { … … 167 282 header( 'Content-Type: text/html; charset=UTF-8' ); 168 283 readfile( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions 169 self::maybe_kick_cron( );284 self::maybe_kick_cron( (int) $config['site_id'] ); 170 285 exit; 171 286 } … … 181 296 * A .php file, so the cache directory's access rules keep it private. 182 297 * 298 * @param int $site_id Network site ID (0 on a single site). 183 299 * @return string 184 300 */ 185 private static function cron_due_file( ) {186 return self::cache_root() . 'cron-due.php';301 private static function cron_due_file( $site_id = 0 ) { 302 return self::cache_root() . ( $site_id ? 'cron-due-' . (int) $site_id . '.php' : 'cron-due.php' ); 187 303 } 188 304 … … 193 309 * @param int $timestamp Unix time of the next run (0 = none pending). 194 310 * @param string $cron_url Absolute URL of wp-cron.php. 195 * @return void 196 */ 197 public static function write_cron_due( $timestamp, $cron_url ) { 311 * @param int $site_id Network site ID (0 on a single site). 312 * @return void 313 */ 314 public static function write_cron_due( $timestamp, $cron_url, $site_id = 0 ) { 198 315 $data = array( 199 316 'due' => (int) $timestamp, 200 317 'url' => (string) $cron_url, 201 318 ); 202 $file = self::cron_due_file( );319 $file = self::cron_due_file( $site_id ); 203 320 if ( is_readable( $file ) ) { 204 321 $current = include $file; … … 224 341 * after the page has been handed to the visitor where the server allows. 225 342 * 226 * @return void 227 */ 228 private static function maybe_kick_cron() { 229 $file = self::cron_due_file(); 343 * @param int $site_id Network site ID of the request (0 on a single site). 344 * @return void 345 */ 346 private static function maybe_kick_cron( $site_id = 0 ) { 347 $file = self::cron_due_file( $site_id ); 230 348 if ( ! is_readable( $file ) ) { 231 349 return; … … 235 353 return; 236 354 } 237 $lock = self::cache_root() . 'cron.lock';355 $lock = self::cache_root() . ( $site_id ? 'cron-' . (int) $site_id . '.lock' : 'cron.lock' ); 238 356 if ( file_exists( $lock ) && ( time() - (int) @filemtime( $lock ) ) < 60 ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors 239 357 return; … … 464 582 public static function path_for_request( array $config ) { 465 583 // phpcs:disable WordPress.Security.ValidatedSanitizedInput -- pre-WP drop-in; values feed a sanitized cache key only. 466 $host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : 'localhost';584 $host = isset( $_SERVER['HTTP_HOST'] ) ? strtolower( $_SERVER['HTTP_HOST'] ) : 'localhost'; // Hosts are case-insensitive; one folder per host. 467 585 $uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '/'; 468 586 $scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] ) ? 'https' : 'http'; … … 588 706 } 589 707 $parts = function_exists( 'wp_parse_url' ) ? wp_parse_url( $url ) : parse_url( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions 590 $host = isset( $parts['host'] ) ? $parts['host'] : '';708 $host = self::url_host( $parts ); 591 709 $path = isset( $parts['path'] ) ? $parts['path'] : '/'; 592 710 $path = str_replace( array( '..', "\0" ), '', $path ); … … 595 713 $dir = self::cache_root() . self::sanitize_segment( $host ) . rtrim( $path, '/' ) . '/'; 596 714 597 // The homepage's variants live directly in the host dir alongside every 598 // other page's SUBdirectory. Recursively deleting the host dir would wipe 599 // the whole site cache — so for the homepage, remove only its own .html 600 // variant files and leave the per-page subdirectories intact. 601 if ( $dir === $host_dir ) { 602 foreach ( (array) glob( $host_dir . '*.html' ) as $file ) { 715 // A site's front page has its variants directly in the site's root 716 // folder, alongside every other page's SUBdirectory. Recursively deleting 717 // that folder would wipe the whole site — so for a front page (the host 718 // root, or a network site's own root like /shop/) remove only its own 719 // .html variant files and leave the per-page subdirectories intact. 720 if ( $dir === $host_dir || in_array( $dir, self::site_roots(), true ) ) { 721 foreach ( (array) glob( $dir . '*.html' ) as $file ) { 603 722 wp_delete_file( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors 604 723 } … … 628 747 // HTML while keeping the hashed assets keeps both sides consistent. 629 748 // (Stale assets are cleared deliberately when CSS/JS settings change.) 630 $keep = array( '.', '..', 'config.php', 'index.html', '.htaccess', 'css', 'js', 'cron-due.php', 'cron.lock' );749 $keep = array( '.', '..', 'config.php', 'index.html', '.htaccess', 'css', 'js', 'cron-due.php', 'cron.lock', 'sites' ); 631 750 foreach ( (array) scandir( $root ) as $item ) { 632 if ( in_array( $item, $keep, true ) ) {751 if ( in_array( $item, $keep, true ) || preg_match( '/^cron(?:-due)?-\d+\.(?:php|lock)$/', (string) $item ) ) { 633 752 continue; 634 753 } … … 642 761 } 643 762 763 /** 764 * Remove cache folders named after a mixed-case Host header. Since 1.1.0 765 * hosts are lowercased, so such folders are never served or purged again. 766 * 767 * @return void 768 */ 769 public static function remove_mixed_case_hosts() { 770 $root = self::cache_root(); 771 if ( ! is_dir( $root ) ) { 772 return; 773 } 774 foreach ( (array) scandir( $root ) as $item ) { 775 if ( '.' !== $item && '..' !== $item && is_dir( $root . $item ) && strtolower( $item ) !== $item ) { 776 self::rrmdir( $root . $item . '/' ); 777 } 778 } 779 } 780 781 /** 782 * Cache folders of every network site's root ("<root>/<host>/<path>/"). 783 * 784 * @return string[] 785 */ 786 public static function site_roots() { 787 $config = self::load_config(); 788 $roots = array(); 789 foreach ( array_keys( (array) $config['sites'] ) as $key ) { 790 $parts = explode( '/', (string) $key, 2 ); 791 $path = isset( $parts[1] ) ? trim( $parts[1], '/' ) : ''; 792 $roots[] = self::cache_root() . self::sanitize_segment( $parts[0] ) . ( '' !== $path ? '/' . $path : '' ) . '/'; 793 } 794 return $roots; 795 } 796 797 /** 798 * Cache folder of a site (host + path), e.g. "<root>/example.com/shop/". 799 * 800 * @param string $host Site host. 801 * @param string $path Site path ('/' or '/shop/'). 802 * @return string 803 */ 804 public static function site_dir( $host, $path ) { 805 $path = trim( str_replace( array( '..', "\0" ), '', (string) $path ), '/' ); 806 $path = preg_replace( '#[^a-zA-Z0-9/_\-.]#', '-', $path ); 807 return self::cache_root() . self::sanitize_segment( strtolower( (string) $host ) ) . ( '' !== $path ? '/' . $path : '' ) . '/'; 808 } 809 810 /** 811 * Purge one network site's pages without touching sites nested inside its 812 * folder (the main site "/" contains the folder of a subdirectory site). 813 * 814 * @param string $dir Site cache folder (from site_dir()). 815 * @return void 816 */ 817 public static function purge_site_dir( $dir ) { 818 $dir = rtrim( (string) $dir, '/' ) . '/'; 819 if ( 0 !== strpos( $dir, self::cache_root() ) || ! is_dir( $dir ) ) { 820 return; 821 } 822 $others = array_values( array_diff( self::site_roots(), array( $dir ) ) ); 823 self::purge_tree( $dir, $others ); 824 } 825 826 /** 827 * Delete cached pages under $dir, skipping (and descending towards) the 828 * folders of other sites. 829 * 830 * @param string $dir Folder with trailing slash. 831 * @param string[] $others Other sites' root folders. 832 * @return void 833 */ 834 private static function purge_tree( $dir, array $others ) { 835 foreach ( (array) scandir( $dir ) as $item ) { 836 if ( '.' === $item || '..' === $item ) { 837 continue; 838 } 839 $path = $dir . $item; 840 if ( ! is_dir( $path ) ) { 841 if ( '.html' === substr( $item, -5 ) && 'index.html' !== $item ) { 842 wp_delete_file( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors 843 } 844 continue; 845 } 846 $sub = $path . '/'; 847 if ( in_array( $sub, $others, true ) ) { 848 continue; // Another site's folder. 849 } 850 $contains_other = false; 851 foreach ( $others as $other ) { 852 if ( 0 === strpos( $other, $sub ) ) { 853 $contains_other = true; 854 break; 855 } 856 } 857 if ( $contains_other ) { 858 self::purge_tree( $sub, $others ); 859 } else { 860 self::rrmdir( $sub ); 861 } 862 } 863 } 864 644 865 /* --------------------------------------------------------------------- * 645 866 * Filesystem helpers … … 684 905 return false; 685 906 } 907 self::forget_compiled( $file ); 686 908 return true; 909 } 910 911 /** 912 * Make PHP's opcode cache drop its compiled copy of a file we just 913 * rewrote. Config and rules files are PHP includes: without this, a server 914 * that only re-checks file times every few seconds (or never, with 915 * opcache.validate_timestamps=0) keeps running the OLD settings — caching 916 * stays off after being switched on, or a new exclusion is ignored. 917 * 918 * @param string $file Absolute path. 919 * @return void 920 */ 921 public static function forget_compiled( $file ) { 922 if ( function_exists( 'opcache_invalidate' ) ) { 923 @opcache_invalidate( $file, true ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- may be restricted by opcache.restrict_api. 924 } 687 925 } 688 926
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)