(int) $zc['ttl'] ) { return; } header( 'X-ZeroConfig-Cache: HIT-WP' ); header( 'Content-Type: text/html; charset=UTF-8' ); header( 'Cache-Control: no-cache, must-revalidate, max-age=0' ); header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', (int) filemtime( $file ) ) . ' GMT' ); $gz = $file . '.gz'; if ( is_file( $gz ) && isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) && false !== strpos( sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ), 'gzip' ) ) { header( 'Content-Encoding: gzip' ); readfile( $gz ); } else { readfile( $file ); } exit; } /* ---------- storing ---------- */ /** Decide at request time (WP loaded) whether the response may be stored. */ public static function storable() { if ( ! ZeroConfig_Options::feature_on( 'cache' ) ) { return false; } if ( is_user_logged_in() || is_admin() || is_feed() || is_404() || is_search() || is_preview() || is_trackback() || is_robots() ) { return false; } if ( 'GET' !== ( isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '' ) ) { return false; } // Only a clean address fills the cache. A request with tracking parameters may still be served // the cached page, but never stores one: whatever it echoes back would reach every visitor. if ( ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only checks that there is no query string return false; } if ( defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE ) { return false; } if ( ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { return false; } if ( isset( $_GET['zc_off'] ) || isset( $_GET['zc_test'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- presence of a flag, nothing is processed return false; } if ( post_password_required() ) { return false; } if ( self::$personal ) { return false; // a cart badge "(1)" or "added to cart" must not be shown to the next visitor } if ( function_exists( 'is_cart' ) && ( is_cart() || is_checkout() || is_account_page() ) ) { return false; } if ( function_exists( 'amp_is_request' ) && amp_is_request() ) { return false; } if ( http_response_code() && 200 !== (int) http_response_code() ) { return false; } foreach ( headers_list() as $h ) { if ( 0 === stripos( $h, 'Set-Cookie:' ) && preg_match( '/wordpress_logged_in|woocommerce|wp_woocommerce_session/', $h ) ) { return false; } if ( 0 === stripos( $h, 'Content-Type:' ) && false === stripos( $h, 'text/html' ) ) { return false; } } return apply_filters( 'zeroconfig_cache_storable', true ); } public static function store( $html ) { if ( strlen( $html ) < 255 || false === stripos( $html, '' ) ) { return; } $cfg = self::config(); $file = self::file_for_request( $cfg ); if ( ! $file ) { return; } if ( null === self::$render_gen || self::generation() !== self::$render_gen ) { return; // unknown start, or purged while this page was rendering: it may be the old content } wp_mkdir_p( dirname( $file ) ); $html .= "\n'; // Each file is written aside and renamed into place, so the web server never sends half of one. $tmp = $file . '.' . wp_generate_password( 12, false ) . '.tmp'; if ( false !== file_put_contents( $tmp, $html ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions rename( $tmp, $file ); if ( function_exists( 'gzencode' ) ) { $gz = gzencode( $html, 6 ); if ( false !== $gz && false !== file_put_contents( $tmp, $gz ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions rename( $tmp, $file . '.gz' ); } } } // A cache miss means PHP is running anyway: use it to clear expired pages, at most every 15 minutes. if ( ! get_transient( 'zeroconfig_gc_lock' ) ) { set_transient( 'zeroconfig_gc_lock', 1, 15 * MINUTE_IN_SECONDS ); self::gc(); } } private static function file_for_request( array $cfg ) { require_once ZEROCONFIG_DIR . 'includes/cache-decide.php'; return zeroconfig_cache_file( $cfg, ZEROCONFIG_CACHE_DIR ); } /* ---------- purging ---------- */ /** * Empty the whole cache. $source says why (turn_on, turn_off, manual, settings, content, site, * stock, edge, api, cli, other): listeners decide from it, e.g. no warm-up after turn_off. */ public static function purge_all( $source = 'other' ) { self::bump_generation(); self::rrmdir( ZEROCONFIG_CACHE_DIR, true ); self::write_config(); do_action( 'zeroconfig_purged', is_string( $source ) ? $source : 'other' ); } /* WordPress hooks pass their own arguments (a theme name, a menu id); these name the source. */ public static function on_site_change() { self::purge_all( 'site' ); } public static function on_content_change() { self::purge_all( 'content' ); } public static function on_deferred() { self::purge_all( 'stock' ); } /* * Purge generation. A page that started rendering before a purge and is stored after it would put * the old HTML back on disk ("I deleted that post and it came back"). Every purge bumps the counter; * a page is stored only if the counter is still what it was when its render began. */ const GEN = 'zeroconfig_purge_gen'; private static $render_gen = null; public static function generation() { wp_cache_delete( self::GEN, 'options' ); // the value another request wrote, not this request's copy return (int) get_option( self::GEN, 0 ); } public static function bump_generation() { update_option( self::GEN, self::generation() + 1, false ); } /** Called when the output buffer starts (template_redirect). */ public static function mark_render_start() { self::$render_gen = self::generation(); // Decided now, before the theme prints (and clears) WooCommerce notices. self::$personal = ( function_exists( 'WC' ) && WC()->cart && WC()->cart->get_cart_contents_count() > 0 ) || ( function_exists( 'wc_notice_count' ) && WC()->session && wc_notice_count() > 0 ); } /** True when the page shows this visitor's cart or a notice meant for them ("added to cart"). */ private static $personal = false; /** Remove every variant (device, language, gzip) of one URL. */ public static function purge_url( $url ) { $p = wp_parse_url( $url ); // Same folder name the cache key uses: the port is part of it ("localhost:8085" → "localhost8085"). $host = isset( $p['host'] ) ? preg_replace( '/[^a-z0-9.\-]/', '', strtolower( $p['host'] . ( isset( $p['port'] ) ? ':' . $p['port'] : '' ) ) ) : ''; $rel = isset( $p['path'] ) ? trim( $p['path'], '/' ) : ''; if ( '' === $host || false !== strpos( $rel, '..' ) ) { return; } // A render of this page that began before the purge must not store the old copy afterwards. Other // pages rendering right now skip their store too: one wasted render, never a wrong page. self::bump_generation(); // The same page may sit under each address the site answers on (example.com and its www twin). $own = self::hosts( true ); foreach ( in_array( $host, $own, true ) ? $own : array( $host ) as $h ) { $dir = ZEROCONFIG_CACHE_DIR . '/' . $h . ( '' !== $rel ? '/' . $rel : '' ); foreach ( (array) glob( $dir . '/index-*.html*' ) as $f ) { wp_delete_file( $f ); } } do_action( 'zeroconfig_purged_url', $url ); } /** Purge a post and the pages that list it (home, blog index, its archives). */ public static function purge_post_urls( $post_id ) { self::purge_url( get_permalink( $post_id ) ); self::purge_url( home_url( '/' ) ); if ( get_option( 'page_for_posts' ) ) { self::purge_url( get_permalink( (int) get_option( 'page_for_posts' ) ) ); } foreach ( (array) get_object_taxonomies( get_post_type( $post_id ) ) as $tax ) { foreach ( (array) wp_get_post_terms( $post_id, $tax ) as $term ) { $link = get_term_link( $term ); if ( ! is_wp_error( $link ) ) { self::purge_url( $link ); } } } $author = get_post_field( 'post_author', $post_id ); if ( $author ) { self::purge_url( get_author_posts_url( (int) $author ) ); } } public static function purge_post( $post_id ) { if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } $status = get_post_status( $post_id ); if ( ! in_array( $status, array( 'publish', 'trash', 'private', 'draft' ), true ) ) { return; } // Content change: purge the post plus every page that lists it. Full purge // only when a post is published/unpublished (menus, widgets, counts change). if ( apply_filters( 'zeroconfig_purge_all_on_save', false, $post_id ) ) { self::purge_all( 'content' ); } else { self::purge_post_urls( $post_id ); } } /** WooCommerce stock or price change (checkout, scheduled sale). Cheap now, complete later. */ public static function on_stock( $product ) { if ( is_numeric( $product ) && function_exists( 'wc_get_product' ) ) { $product = wc_get_product( (int) $product ); // the scheduled-sale hooks pass an id } if ( ! is_object( $product ) || ! method_exists( $product, 'get_id' ) ) { return; } $id = method_exists( $product, 'get_parent_id' ) && $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); self::purge_post_urls( (int) $id ); if ( function_exists( 'wc_get_page_permalink' ) ) { self::purge_url( wc_get_page_permalink( 'shop' ) ); // lists stock status and "Add to cart" } self::purge_all_later(); } /** A batch of products whose sale started or ended. */ public static function on_products( $ids ) { foreach ( array_slice( (array) $ids, 0, 50 ) as $id ) { // the deferred full purge covers any beyond self::on_stock( (int) $id ); } self::purge_all_later(); } /** * Full purge in the background, about 30 s from now. A burst (an order with 10 products, a stock * import) collapses into one run. Never inside the visitor's request. */ public static function purge_all_later( $delay = 30 ) { if ( ! wp_next_scheduled( self::DEFERRED ) ) { wp_schedule_single_event( time() + $delay, self::DEFERRED ); } } /** * An image already in use was replaced or edited (a media-replace plugin, regenerated sizes, new alt * text): pages showing it need its new address. Fresh uploads are skipped: WordPress saves their * metadata once per generated size, and no page shows them yet. */ public static function on_attachment( $id ) { $post = get_post( (int) $id ); if ( $post && 'attachment' === $post->post_type && time() - (int) get_post_time( 'U', true, $post ) > 600 ) { self::purge_all_later(); } } public static function on_attachment_meta( $meta_id, $post_id, $key ) { if ( '_wp_attachment_metadata' === $key || '_wp_attached_file' === $key ) { self::on_attachment( $post_id ); } } public static function on_status_change( $new, $old, $post ) { if ( $new === $old || ! $post instanceof WP_Post || wp_is_post_revision( $post->ID ) ) { return; } if ( 'publish' === $new || 'publish' === $old ) { self::purge_all( 'content' ); } } /** Only approved comments change the public page. Spam and pending do not. */ public static function on_comment( $comment_id, $approved = null ) { $c = get_comment( $comment_id ); if ( ! $c ) { return; } if ( null !== $approved && 1 !== (int) $approved && 'approve' !== $approved && 'approved' !== $approved ) { return; } self::purge_url( get_permalink( (int) $c->comment_post_ID ) ); } public static function on_comment_status( $new, $old, $comment ) { if ( 'approved' === $new || 'approved' === $old ) { self::purge_url( get_permalink( (int) $comment->comment_post_ID ) ); } } public static function hook_purges() { add_action( 'save_post', array( __CLASS__, 'purge_post' ) ); // Not deleted_post: by then the post is gone, its status reads false and nothing would be purged. add_action( 'before_delete_post', array( __CLASS__, 'purge_post' ) ); add_action( 'transition_post_status', array( __CLASS__, 'on_status_change' ), 10, 3 ); add_action( 'comment_post', array( __CLASS__, 'on_comment' ), 10, 2 ); add_action( 'transition_comment_status', array( __CLASS__, 'on_comment_status' ), 10, 3 ); foreach ( array( 'switch_theme', 'customize_save_after', 'wp_update_nav_menu', 'update_option_sidebars_widgets', 'activated_plugin', 'deactivated_plugin', 'upgrader_process_complete' ) as $hook ) { add_action( $hook, array( __CLASS__, 'on_site_change' ) ); } // Stock changes happen inside the checkout request: purge only the product's own pages there, // and leave the full purge (related products, shortcodes, widgets) to a background run. add_action( 'woocommerce_product_set_stock', array( __CLASS__, 'on_stock' ) ); add_action( 'woocommerce_variation_set_stock', array( __CLASS__, 'on_stock' ) ); // Scheduled sales start and end from WooCommerce's own cron: prices change without save_post. // After WooCommerce's handler (priority 10) has written the new price. add_action( 'wc_product_start_scheduled_sale', array( __CLASS__, 'on_stock' ), 20 ); add_action( 'wc_product_end_scheduled_sale', array( __CLASS__, 'on_stock' ), 20 ); add_action( 'wc_after_products_starting_sales', array( __CLASS__, 'on_products' ) ); add_action( 'wc_after_products_ending_sales', array( __CLASS__, 'on_products' ) ); // Elementor "Regenerate CSS": every page's CSS file name changes. add_action( 'elementor/core/files/clear_cache', array( __CLASS__, 'on_site_change' ) ); add_action( self::DEFERRED, array( __CLASS__, 'on_deferred' ) ); add_action( 'edited_term', array( __CLASS__, 'on_content_change' ) ); add_action( 'edit_attachment', array( __CLASS__, 'on_attachment' ) ); add_action( 'updated_post_meta', array( __CLASS__, 'on_attachment_meta' ), 10, 3 ); } /* ---------- config file read by the drop-in ---------- */ /** * How long a cached page may be served. A page carries WordPress nonces for * guest forms; a guest nonce is valid for between nonce_life/2 and * nonce_life. Serving a page longer than nonce_life/2 can hand a visitor an * expired nonce ("Security check failed"). Keep a margin for the hourly * clean-up, so: min(cache_ttl, nonce_life/2 - margin), margin ≤ 3 h. */ public static function effective_ttl() { $nonce = (int) apply_filters( 'nonce_life', DAY_IN_SECONDS, '' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- reads WordPress core's own filter $half = max( 120, intdiv( $nonce, 2 ) ); $margin = min( 3 * HOUR_IN_SECONDS, intdiv( $half, 2 ) ); $ttl = min( (int) ZeroConfig_Options::get( 'cache_ttl', 36000 ), $half - $margin ); return (int) apply_filters( 'zeroconfig_cache_ttl', max( 60, $ttl ) ); } public static function config() { $exclude = array( '/cart', '/checkout', '/my-account', '/account', '/login', '/register', '/wp-admin' ); return array( 'enabled' => ZeroConfig_Options::feature_on( 'cache' ), // The drop-in serves nothing once this file is gone: deleting or renaming the plugin folder // (what people do when wp-admin is unreachable) must bring the plain site back at once. // Relative to the plugins folder: the file is public, so it must not show the server's paths. 'plugin' => plugin_basename( ZEROCONFIG_FILE ), 'ttl' => self::effective_ttl(), 'hosts' => self::hosts( true ), 'langs' => self::languages(), 'exclude' => apply_filters( 'zeroconfig_cache_exclude', $exclude ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- our own setting (paths never cached), not a query ); } /** Language codes the site really has (Polylang, WPML): the only language cookie values that pick a cache copy. */ public static function languages() { $langs = array(); if ( function_exists( 'pll_languages_list' ) ) { $langs = (array) pll_languages_list( array( 'fields' => 'slug' ) ); } $wpml = apply_filters( 'wpml_active_languages', null, array( 'skip_missing' => 0 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own filter if ( is_array( $wpml ) ) { $langs = array_merge( $langs, array_keys( $wpml ) ); } $langs = array_filter( array_map( 'strval', $langs ), function ( $l ) { return (bool) preg_match( '/^[A-Za-z0-9_-]{1,12}$/', $l ); } ); return array_values( array_unique( $langs ) ); } /** * The addresses this site answers on (home and WordPress address, port included), e.g. * "example.com" or "localhost:8085". Only these get a cache folder. Sites that serve one install * on several domains (a domain per language) add theirs with the zeroconfig_cache_hosts filter. * $folder: in the folder-name form the cache uses (same clean-up as the cache key, so no ":"). */ public static function hosts( $folder = false ) { $hosts = array(); foreach ( array( home_url(), site_url() ) as $u ) { $p = wp_parse_url( $u ); if ( empty( $p['host'] ) ) { continue; } // Browsers send an international name in its ASCII form: münchen.de arrives as xn--mnchen-3ya.de. $name = rtrim( self::ascii_host( strtolower( $p['host'] ) ), '.' ); // "example.com." is example.com $port = isset( $p['port'] ) ? ':' . $p['port'] : ''; $hosts[] = $name . $port; // The www twin, for sites that answer on both without redirecting one to the other. if ( false !== strpos( $name, '.' ) && ! filter_var( $name, FILTER_VALIDATE_IP ) ) { $hosts[] = ( 0 === strpos( $name, 'www.' ) ? substr( $name, 4 ) : 'www.' . $name ) . $port; } } $hosts = array_values( array_unique( array_filter( array_map( 'strtolower', (array) apply_filters( 'zeroconfig_cache_hosts', $hosts ) ) ) ) ); if ( $folder ) { $hosts = array_values( array_unique( preg_replace( '/[^a-z0-9.\-]/', '', $hosts ) ) ); } return $hosts; } /** "münchen.de" → "xn--mnchen-3ya.de"; ASCII names come back unchanged. */ public static function ascii_host( $host ) { if ( ! preg_match( '/[^\x00-\x7f]/', $host ) ) { return $host; } if ( function_exists( 'idn_to_ascii' ) ) { $a = idn_to_ascii( $host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 ); if ( $a ) { return strtolower( $a ); } } // Bundled with WordPress, for servers without intl: the namespaced class since 6.2, the old name before. foreach ( array( 'WpOrg\Requests\IdnaEncoder', 'Requests_IDNAEncoder' ) as $encoder ) { if ( class_exists( $encoder ) ) { try { return strtolower( $encoder::encode( $host ) ); } catch ( Exception $e ) { return $host; } } } return $host; } /** Delete expired pages. Runs hourly and, at most every 15 minutes, after a page is stored. */ public static function gc() { if ( ! is_dir( ZEROCONFIG_CACHE_DIR ) ) { return 0; } $ttl = self::effective_ttl(); $n = 0; foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( ZEROCONFIG_CACHE_DIR, FilesystemIterator::SKIP_DOTS ) ) as $f ) { if ( preg_match( '/\.(html|gz)$/', $f->getFilename() ) && ( time() - $f->getMTime() ) > $ttl ) { wp_delete_file( $f->getPathname() ); $n++; } } return $n; } /** * The drop-in's settings, as data (JSON): the cache folder holds no PHP. It sits next to the * public cache files and says nothing private (switch, lifetime, host names, excluded paths). */ public static function write_config() { wp_mkdir_p( ZEROCONFIG_CACHE_DIR ); $json = wp_json_encode( self::config() ); $cur = ZEROCONFIG_CACHE_DIR . '/config.json'; if ( ! is_file( $cur ) || file_get_contents( $cur ) !== $json ) { @file_put_contents( $cur, $json ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( class_exists( 'ZeroConfig_Htaccess' ) && ZeroConfig_Htaccess::installed() ) { ZeroConfig_Htaccess::install(); // exclusions changed: rules follow } } // 1.0.0 wrote these two as PHP. foreach ( array( 'config.php', 'index.php' ) as $old ) { if ( is_file( ZEROCONFIG_CACHE_DIR . '/' . $old ) ) { wp_delete_file( ZEROCONFIG_CACHE_DIR . '/' . $old ); } } if ( ! is_file( ZEROCONFIG_CACHE_DIR . '/index.html' ) ) { @file_put_contents( ZEROCONFIG_CACHE_DIR . '/index.html', '' ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- no folder listing } self::ping_token(); } /** The drop-in's settings as an array, or null when there are none yet. */ public static function read_config() { $f = ZEROCONFIG_CACHE_DIR . '/config.json'; $zc = is_file( $f ) ? json_decode( (string) file_get_contents( $f ), true ) : null; return is_array( $zc ) ? $zc : null; } /** * Random token in cache/zeroconfig/ping.txt. The .htaccess rules map /zc-ping.txt to it, so when that * address answers with this token, mod_rewrite, .htaccess and the cache folder layout all work, * whatever modules the host has. Created when missing (a purge empties the folder). */ public static function ping_token() { $f = ZEROCONFIG_CACHE_DIR . '/ping.txt'; $t = is_file( $f ) ? trim( (string) file_get_contents( $f ) ) : ''; if ( '' === $t ) { $t = wp_generate_password( 24, false ); wp_mkdir_p( ZEROCONFIG_CACHE_DIR ); @file_put_contents( $f, $t ); // phpcs:ignore WordPress.WP.AlternativeFunctions } return $t; } /* ---------- drop-in installation ---------- */ public static function install_dropin() { $target = WP_CONTENT_DIR . '/advanced-cache.php'; if ( is_file( $target ) && false === strpos( (string) file_get_contents( $target ), self::DROPIN_MARKER ) ) { return new WP_Error( 'zc_dropin_foreign', __( 'Another caching plugin owns advanced-cache.php. Deactivate it first.', 'zeroconfig' ) ); } if ( ! @copy( ZEROCONFIG_DIR . 'includes/advanced-cache.php', $target ) ) { return new WP_Error( 'zc_dropin_write', __( 'wp-content is not writable. Cache will be served by WordPress instead.', 'zeroconfig' ) ); } self::write_config(); $htaccess = ZeroConfig_Htaccess::install(); $wpconfig = self::set_wp_cache_constant( true ); if ( is_wp_error( $wpconfig ) ) { return $wpconfig; } return is_wp_error( $htaccess ) ? $htaccess : true; } /** * After a plugin update the installed drop-in is still the old copy: it was only copied when the * engine was turned on. Replace it when it differs from the bundled one (ours only, never a * foreign advanced-cache.php). Cheap: two small file hashes. Returns true when it was refreshed. */ public static function refresh_dropin() { $target = WP_CONTENT_DIR . '/advanced-cache.php'; $source = ZEROCONFIG_DIR . 'includes/advanced-cache.php'; if ( ! is_file( $target ) || false === strpos( (string) file_get_contents( $target ), self::DROPIN_MARKER ) ) { return false; } if ( md5_file( $target ) === md5_file( $source ) ) { return false; } $tmp = $target . '.zc-tmp'; if ( ! @copy( $source, $tmp ) || ! @rename( $tmp, $target ) ) { // rename: never a half-written drop-in wp_delete_file( $tmp ); return false; } self::write_config(); return true; } /** * Called on every request that reaches WordPress. The drop-in is a copy, so after a plugin update * (or files replaced over FTP or git, which no hook announces) the old code keeps deciding the * cache key until someone opens wp-admin. Two stat() calls notice it; the hashes are compared * only when the bundled file is newer, and a copy that is identical just gets its time aligned. */ public static function maybe_refresh_dropin() { $target = WP_CONTENT_DIR . '/advanced-cache.php'; $source = ZEROCONFIG_DIR . 'includes/advanced-cache.php'; $t = @filemtime( $target ); // phpcs:ignore WordPress.PHP.NoSilencedErrors $s = @filemtime( $source ); // phpcs:ignore WordPress.PHP.NoSilencedErrors if ( ! $t || ! $s || $s <= $t || get_transient( 'zeroconfig_dropin_retry' ) ) { return false; } if ( self::refresh_dropin() ) { ZeroConfig_Htaccess::refresh(); // a new version may bring new rules too return true; } if ( is_file( $target ) && md5_file( $target ) === md5_file( $source ) ) { @touch( $target ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- same content, newer time: stop checking } else { set_transient( 'zeroconfig_dropin_retry', 1, 10 * MINUTE_IN_SECONDS ); // not writable now: do not try on every request } return false; } public static function remove_dropin() { $target = WP_CONTENT_DIR . '/advanced-cache.php'; if ( is_file( $target ) && false !== strpos( (string) file_get_contents( $target ), self::DROPIN_MARKER ) ) { wp_delete_file( $target ); } ZeroConfig_Htaccess::remove(); self::set_wp_cache_constant( false ); } /** * Pure text edit of wp-config.php source. Only ever touches the line we added (it carries our * marker); a WP_CACHE defined by another plugin or the host is theirs and stays as it is. * Returns the new source, or WP_Error when someone else has WP_CACHE switched off. */ public static function edit_wp_cache_line( $src, $on ) { $ours = "/^[ \t]*define\s*\(\s*['\"]WP_CACHE['\"]\s*,[^;]+;[ \t]*\/\/ Added by ZeroConfig[^\r\n]*\R?/mi"; $out = preg_replace( $ours, '', $src ); $others = preg_match( "/^[ \t]*define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*([^)]+)\)/mi", $out, $m ); if ( ! $on ) { return $out; } if ( $others ) { if ( ! in_array( strtolower( trim( $m[1] ) ), array( 'true', '1' ), true ) ) { return new WP_Error( 'zc_wpconfig', __( 'WP_CACHE is switched off in wp-config.php by your host or another plugin. Cache will be served by WordPress instead.', 'zeroconfig' ) ); } return $out; // already on, defined by someone else: nothing to add, nothing of ours to remove later } $line = "define( 'WP_CACHE', true ); // Added by ZeroConfig\n"; // Right after PHP's opening tag, wherever the file puts it (a byte-order mark, blank lines, isDir() ? @rmdir( $f->getPathname() ) : wp_delete_file( $f->getPathname() ); } if ( ! $keep_root ) { @rmdir( $dir ); } } public static function size() { $bytes = 0; $files = 0; if ( is_dir( ZEROCONFIG_CACHE_DIR ) ) { foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( ZEROCONFIG_CACHE_DIR, FilesystemIterator::SKIP_DOTS ) ) as $f ) { if ( 0 === strpos( $f->getFilename(), 'index-' ) && '.html' === substr( $f->getFilename(), -5 ) ) { $files++; $bytes += $f->getSize(); } } } return array( 'files' => $files, 'bytes' => $bytes ); } }