Plugin Directory


Ignore:
Timestamp:
10/13/2026 10:00:51 AM (13 days ago)
Author:
shiponkarmakar
Message:

Release v1.1.0

File:
1 edited

Legend:

Unmodified
Added
Removed
  • optimazz/trunk/includes/Modules/Cache/engine.php

    r3723128 r3743505  
    3838                        'site_host'            => '',
    3939                        '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,
    4045                );
    4146        }
    … …  
    7782
    7883        /**
     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        /**
    79190         * Persist config to disk as a returnable PHP array (fast to include).
    80191         *
    … …  
    87198                // var_export is used to serialize the config to a fast include file, not for debugging.
    88199                $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                }
    89203                return self::atomic_write( self::config_file(), $code );
    90204        }
    … …  
    144258         */
    145259        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'] : '/' );
    147262
    148263                if ( empty( $config['enabled'] ) ) {
    … …  
    167282                                header( 'Content-Type: text/html; charset=UTF-8' );
    168283                                readfile( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions
    169                                 self::maybe_kick_cron();
     284                                self::maybe_kick_cron( (int) $config['site_id'] );
    170285                                exit;
    171286                        }
    … …  
    181296         * A .php file, so the cache directory's access rules keep it private.
    182297         *
     298         * @param int $site_id Network site ID (0 on a single site).
    183299         * @return string
    184300         */
    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' );
    187303        }
    188304
    … …  
    193309         * @param int    $timestamp Unix time of the next run (0 = none pending).
    194310         * @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 ) {
    198315                $data = array(
    199316                        'due' => (int) $timestamp,
    200317                        'url' => (string) $cron_url,
    201318                );
    202                 $file = self::cron_due_file();
     319                $file = self::cron_due_file( $site_id );
    203320                if ( is_readable( $file ) ) {
    204321                        $current = include $file;
    … …  
    224341         * after the page has been handed to the visitor where the server allows.
    225342         *
    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 );
    230348                if ( ! is_readable( $file ) ) {
    231349                        return;
    … …  
    235353                        return;
    236354                }
    237                 $lock = self::cache_root() . 'cron.lock';
     355                $lock = self::cache_root() . ( $site_id ? 'cron-' . (int) $site_id . '.lock' : 'cron.lock' );
    238356                if ( file_exists( $lock ) && ( time() - (int) @filemtime( $lock ) ) < 60 ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors
    239357                        return;
    … …  
    464582        public static function path_for_request( array $config ) {
    465583                // 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.
    467585                $uri    = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '/';
    468586                $scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] ) ? 'https' : 'http';
    … …  
    588706                }
    589707                $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 );
    591709                $path     = isset( $parts['path'] ) ? $parts['path'] : '/';
    592710                $path     = str_replace( array( '..', "\0" ), '', $path );
    … …  
    595713                $dir      = self::cache_root() . self::sanitize_segment( $host ) . rtrim( $path, '/' ) . '/';
    596714
    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 ) {
    603722                                wp_delete_file( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
    604723                        }
    … …  
    628747                //    HTML while keeping the hashed assets keeps both sides consistent.
    629748                //    (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' );
    631750                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 ) ) {
    633752                                continue;
    634753                        }
    … …  
    642761        }
    643762
     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
    644865        /* --------------------------------------------------------------------- *
    645866         *  Filesystem helpers
    … …  
    684905                        return false;
    685906                }
     907                self::forget_compiled( $file );
    686908                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                }
    687925        }
    688926
Note: See TracChangeset for help on using the changeset viewer.