string, * 'status' => 'executed'|'warning'|'not_detected'|'failed'|'skipped', * 'ok' => bool, // convenience mirror: status in executed|warning. * 'detail' => string, * ] */ class OVKIT_Cache_Manager { const NONCE_ACTION = 'ovkit_cache_manager'; const AJAX_CLEAR = 'ovkit_cache_clear'; const AJAX_LOG = 'ovkit_cache_log'; const AJAX_ENV = 'ovkit_cache_env'; const CAPABILITY = 'manage_options'; const BUST_OPTION = 'ovkit_cache_bust_version'; const LOG_OPTION = 'ovkit_cache_log'; const LOG_LIMIT = 20; const STATUS_EXECUTED = 'executed'; const STATUS_WARNING = 'warning'; const STATUS_NOT_DETECTED = 'not_detected'; const STATUS_FAILED = 'failed'; const STATUS_SKIPPED = 'skipped'; /** * Singleton instance. * * @var self|null */ private static $instance = null; /** * Whether boot() already registered the hooks. * * @var bool */ private $booted = false; /** * Cached browser cache-bust version (null until read). * * @var int|null */ private $bust_version = null; /** * Instance. * * @return self */ public static function instance(): self { if ( ! self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Boot. */ public function boot(): void { if ( $this->booted ) { return; } $this->booted = true; add_action( 'admin_bar_menu', array( $this, 'render_admin_bar' ), 110 ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'wp_ajax_' . self::AJAX_CLEAR, array( $this, 'ajax_clear' ) ); add_action( 'wp_ajax_' . self::AJAX_LOG, array( $this, 'ajax_log' ) ); add_action( 'wp_ajax_' . self::AJAX_ENV, array( $this, 'ajax_env' ) ); add_filter( 'style_loader_src', array( $this, 'filter_asset_src' ), 9999 ); add_filter( 'script_loader_src', array( $this, 'filter_asset_src' ), 9999 ); } // Row builder. /** * Row. * * @param string $label Label. * @param string $status Status. * @param string $detail Detail. * @return array */ private function row( string $label, string $status, string $detail ): array { $ok = in_array( $status, array( self::STATUS_EXECUTED, self::STATUS_WARNING ), true ); return array( 'label' => $label, 'status' => $status, 'ok' => $ok, 'detail' => $detail, ); } // Admin bar. /** * Render admin bar. * * @param mixed $bar Bar. */ public function render_admin_bar( $bar ): void { if ( ! current_user_can( self::CAPABILITY ) ) { return; } if ( ! is_admin_bar_showing() ) { return; } if ( ! $bar->get_node( 'ovkit' ) ) { $bar->add_node( array( 'id' => 'ovkit', 'title' => 'Toolkit', 'href' => admin_url( 'admin.php?page=ovkit' ), ) ); } $bar->add_node( array( 'id' => 'ovkit-cache', 'parent' => 'ovkit', 'title' => __( 'Cache', 'ovanap-admin-toolkit' ), 'href' => '#', 'meta' => array( 'class' => 'ovkit-cache-root' ), ) ); $systems = $this->detect_systems(); $env = $this->get_environment(); $bust_label = $this->bust_label( $env['bust_version'], $env['bust_human'] ); $items = array( array( 'id' => 'ovkit-cache-env', 'label' => __( 'Systems & environment…', 'ovanap-admin-toolkit' ), ), array( 'id' => 'ovkit-cache-preview', 'label' => __( 'Preview (dry-run all safe)', 'ovanap-admin-toolkit' ), ), array( 'id' => 'ovkit-cache-all-safe', 'label' => __( 'Clear All — Safe', 'ovanap-admin-toolkit' ), ), array( 'id' => 'ovkit-cache-all-aggressive', 'label' => __( 'Clear All — Aggressive…', 'ovanap-admin-toolkit' ), 'aggressive' => true, ), array( 'id' => 'ovkit-cache-object-runtime', 'label' => __( 'Object cache — runtime only', 'ovanap-admin-toolkit' ), ), array( 'id' => 'ovkit-cache-object-full', 'label' => __( 'Object cache — full flush…', 'ovanap-admin-toolkit' ), 'aggressive' => true, ), array( 'id' => 'ovkit-cache-transients', 'label' => __( 'Clear Transients', 'ovanap-admin-toolkit' ), ), array( 'id' => 'ovkit-cache-wp-core', 'label' => __( 'Clear WordPress Core Caches', 'ovanap-admin-toolkit' ), ), array( 'id' => 'ovkit-cache-page', 'label' => $this->label_for( 'page', $systems, __( 'Clear Page Cache', 'ovanap-admin-toolkit' ) ), ), array( 'id' => 'ovkit-cache-builder', 'label' => $this->label_for( 'builder', $systems, __( 'Clear Builder / Asset Cache', 'ovanap-admin-toolkit' ) ), ), ); if ( class_exists( 'WooCommerce' ) ) { $items[] = array( 'id' => 'ovkit-cache-woo', 'label' => __( 'Invalidate WooCommerce transient versions', 'ovanap-admin-toolkit' ), ); } $items[] = array( 'id' => 'ovkit-cache-opcache', 'label' => __( 'Reset OPcache', 'ovanap-admin-toolkit' ), ); $items[] = array( 'id' => 'ovkit-cache-rewrite', 'label' => __( 'Flush Rewrite Rules (expensive)', 'ovanap-admin-toolkit' ), 'aggressive' => true, ); $items[] = array( 'id' => 'ovkit-cache-browser', 'label' => $bust_label, ); $items[] = array( 'id' => 'ovkit-cache-log', 'label' => __( 'Recent activity…', 'ovanap-admin-toolkit' ), ); foreach ( $items as $item ) { $classes = array( 'ovkit-cache-action' ); if ( ! empty( $item['aggressive'] ) ) { $classes[] = 'ovkit-cache-aggressive'; } $bar->add_node( array( 'id' => $item['id'], 'parent' => 'ovkit-cache', 'title' => esc_html( $item['label'] ), 'href' => '#', 'meta' => array( 'class' => implode( ' ', $classes ), 'onclick' => 'return false;', 'tabindex' => '0', ), ) ); } // Read-only footer: multisite / persistent cache hints. $hints = array(); if ( $env['multisite'] ) { $hints[] = __( 'Multisite network', 'ovanap-admin-toolkit' ); } if ( $env['persistent_object_cache'] ) { /* translators: %s: object cache backend name */ $hints[] = sprintf( __( 'Persistent object cache: %s', 'ovanap-admin-toolkit' ), $env['object_cache_backend'] ); } if ( $hints ) { $bar->add_node( array( 'id' => 'ovkit-cache-hint', 'parent' => 'ovkit-cache', 'title' => '' . esc_html( implode( ' · ', $hints ) ) . '', 'href' => false, 'meta' => array( 'class' => 'ovkit-cache-hint' ), ) ); } } /** * Bust label. * * @param int $version Version. * @param string $human Human. * @return string */ private function bust_label( int $version, string $human ): string { if ( $version <= 0 ) { return __( 'Bump Browser Cache (assets) — not yet set', 'ovanap-admin-toolkit' ); } /* translators: 1: version integer, 2: human-readable time-ago */ return sprintf( __( 'Bump Browser Cache — v=%1$d (%2$s)', 'ovanap-admin-toolkit' ), $version, $human ); } /** * Label for. * * @param string $scope Scope. * @param array $systems Systems. * @param string $fallback Fallback. * @return string */ private function label_for( string $scope, array $systems, string $fallback ): string { $matching = array(); foreach ( $systems as $sys ) { if ( in_array( $scope, $sys['scopes'], true ) ) { $matching[] = $sys['label']; } } if ( empty( $matching ) ) { return $fallback; } /* translators: 1: base action label, 2: detected plugin name(s) */ return sprintf( __( '%1$s (%2$s)', 'ovanap-admin-toolkit' ), $fallback, implode( ', ', $matching ) ); } // Assets. /** * Enqueue assets. */ public function enqueue_assets(): void { if ( ! current_user_can( self::CAPABILITY ) ) { return; } if ( ! is_admin_bar_showing() ) { return; } wp_enqueue_style( 'ovkit-cache-manager', OVKIT_URL . 'assets/cache-manager/cache-manager.css', array(), OVKIT_VERSION ); wp_enqueue_script( 'ovkit-cache-manager', OVKIT_URL . 'assets/cache-manager/cache-manager.js', array(), OVKIT_VERSION, true ); $env = $this->get_environment(); wp_localize_script( 'ovkit-cache-manager', 'ovkitCacheManager', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'actions' => array( 'clear' => self::AJAX_CLEAR, 'log' => self::AJAX_LOG, 'env' => self::AJAX_ENV, ), 'nonce' => wp_create_nonce( self::NONCE_ACTION ), 'env' => $env, 'i18n' => array( 'working' => __( 'Clearing…', 'ovanap-admin-toolkit' ), 'previewing' => __( 'Previewing…', 'ovanap-admin-toolkit' ), 'loading' => __( 'Loading…', 'ovanap-admin-toolkit' ), 'done' => __( 'Cache cleared', 'ovanap-admin-toolkit' ), 'partial' => __( 'Finished with warnings', 'ovanap-admin-toolkit' ), 'failed' => __( 'Cache clear failed', 'ovanap-admin-toolkit' ), 'nothing' => __( 'Nothing to clear', 'ovanap-admin-toolkit' ), 'previewTitle' => __( 'Preview — would clear:', 'ovanap-admin-toolkit' ), 'previewEmpty' => __( 'Preview — nothing would be cleared', 'ovanap-admin-toolkit' ), 'runNow' => __( 'Clear now', 'ovanap-admin-toolkit' ), 'cancel' => __( 'Cancel', 'ovanap-admin-toolkit' ), 'netError' => __( 'Network error — check connection', 'ovanap-admin-toolkit' ), 'forbidden' => __( 'Not allowed', 'ovanap-admin-toolkit' ), 'confirmTitle' => __( 'This is an aggressive cache action', 'ovanap-admin-toolkit' ), 'confirmAggressiveAll' => __( 'Clear All — Aggressive also flushes the full object cache, rewrite rules, and OPcache. This is heavier than the safe run.', 'ovanap-admin-toolkit' ), 'confirmObjectFull' => __( 'Full object cache flush removes every entry from the configured backend.', 'ovanap-admin-toolkit' ), 'confirmRewrite' => __( 'Rewrite rules flush is an expensive operation on large sites.', 'ovanap-admin-toolkit' ), /* translators: %s: object cache backend name */ 'warnPersistent' => __( 'Persistent object cache detected (%s). A full flush may affect other sites sharing this backend.', 'ovanap-admin-toolkit' ), 'warnMultisite' => __( 'This is a multisite network — the flush affects every site.', 'ovanap-admin-toolkit' ), 'envTitle' => __( 'Detected environment', 'ovanap-admin-toolkit' ), 'logTitle' => __( 'Recent cache activity', 'ovanap-admin-toolkit' ), 'logEmpty' => __( 'No recorded activity yet', 'ovanap-admin-toolkit' ), 'status' => array( 'executed' => __( 'executed', 'ovanap-admin-toolkit' ), 'warning' => __( 'warning', 'ovanap-admin-toolkit' ), 'not_detected' => __( 'not detected', 'ovanap-admin-toolkit' ), 'failed' => __( 'failed', 'ovanap-admin-toolkit' ), 'skipped' => __( 'skipped', 'ovanap-admin-toolkit' ), ), /* translators: 1: executed count, 2: warning count, 3: skipped count, 4: failed count */ 'summaryFmt' => __( '%1$d executed · %2$d warnings · %3$d skipped · %4$d failed', 'ovanap-admin-toolkit' ), 'safeSkippedNote' => __( 'Aggressive items not run in safe mode: full object flush, rewrite rules, OPcache.', 'ovanap-admin-toolkit' ), ), ) ); } // Browser bust filter. /** * Filter asset src. * * @param mixed $src Src. */ public function filter_asset_src( $src ) { if ( ! is_string( $src ) || '' === $src ) { return $src; } if ( null === $this->bust_version ) { $this->bust_version = (int) get_option( self::BUST_OPTION, 0 ); } if ( $this->bust_version <= 0 ) { return $src; } $home = home_url(); $site = site_url(); if ( strpos( $src, $home ) !== 0 && strpos( $src, $site ) !== 0 && strpos( $src, '/' ) !== 0 ) { return $src; } return add_query_arg( 'ovkbust', $this->bust_version, $src ); } // AJAX: clear. /** * Ajax clear. */ public function ajax_clear(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_send_json_error( array( 'message' => __( 'Forbidden', 'ovanap-admin-toolkit' ) ), 403 ); } check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : ''; $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : 'run'; $preview = ( 'preview' === $mode ); $allowed = array( 'all_safe', 'all_aggressive', 'object_runtime', 'object_full', 'transients', 'rewrite', 'opcache', 'page', 'builder', 'wordpress_core', 'woocommerce', 'browser_bust', ); if ( ! in_array( $scope, $allowed, true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid scope', 'ovanap-admin-toolkit' ) ), 400 ); } try { $results = $this->run( $scope, $preview ); } catch ( \Throwable $e ) { if ( function_exists( 'ovkit_log' ) ) { ovkit_log( 'Cache manager error: ' . $e->getMessage(), array( 'scope' => $scope, 'mode' => $mode, ), 'error' ); } wp_send_json_error( array( 'message' => __( 'Cache clear failed', 'ovanap-admin-toolkit' ) ), 500 ); } $summary = $this->summarize( $results ); // Notes communicate skipped-in-safe and aggressive warnings. $notes = array(); if ( ! $preview && 'all_safe' === $scope ) { $notes[] = __( 'Safe mode: full object flush, rewrite rules, and OPcache were not run.', 'ovanap-admin-toolkit' ); } if ( ! $preview ) { $this->append_log( $scope, $mode, $results, $summary ); } wp_send_json_success( array( 'scope' => $scope, 'mode' => $preview ? 'preview' : 'run', 'results' => $results, 'summary' => $summary, 'notes' => $notes, ) ); } // AJAX: log. /** * Ajax log. */ public function ajax_log(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_send_json_error( array( 'message' => __( 'Forbidden', 'ovanap-admin-toolkit' ) ), 403 ); } check_ajax_referer( self::NONCE_ACTION, 'nonce' ); $log = (array) get_option( self::LOG_OPTION, array() ); $log = array_reverse( $log ); // newest first. foreach ( $log as &$entry ) { if ( ! empty( $entry['time'] ) ) { $entry['human'] = human_time_diff( (int) $entry['time'], time() ); } } wp_send_json_success( array( 'entries' => $log ) ); } // AJAX: env. /** * Ajax env. */ public function ajax_env(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_send_json_error( array( 'message' => __( 'Forbidden', 'ovanap-admin-toolkit' ) ), 403 ); } check_ajax_referer( self::NONCE_ACTION, 'nonce' ); wp_send_json_success( array( 'env' => $this->get_environment(), 'systems' => $this->detect_systems(), ) ); } // Environment. /** * Get environment. * * @return array */ public function get_environment(): array { $persistent = function_exists( 'wp_using_ext_object_cache' ) && wp_using_ext_object_cache(); $backend = $persistent ? $this->detect_object_cache_backend() : 'none'; $bust = (int) get_option( self::BUST_OPTION, 0 ); return array( 'multisite' => is_multisite(), 'persistent_object_cache' => $persistent, 'object_cache_backend' => $backend, 'has_runtime_flush' => function_exists( 'wp_cache_flush_runtime' ), 'has_opcache' => function_exists( 'opcache_reset' ), 'server_software' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', 'woocommerce_active' => class_exists( 'WooCommerce' ), 'bust_version' => $bust, 'bust_human' => $bust > 0 ? human_time_diff( $bust, time() ) . ' ' . __( 'ago', 'ovanap-admin-toolkit' ) : '', ); } /** * Detect object cache backend. * * @return string */ private function detect_object_cache_backend(): string { global $wp_object_cache; if ( is_object( $wp_object_cache ) ) { $class = get_class( $wp_object_cache ); if ( stripos( $class, 'redis' ) !== false ) { return 'Redis'; } if ( stripos( $class, 'memcached' ) !== false ) { return 'Memcached'; } if ( stripos( $class, 'memcache' ) !== false ) { return 'Memcache'; } if ( stripos( $class, 'apcu' ) !== false ) { return 'APCu'; } if ( stripos( $class, 'w3' ) !== false ) { return 'W3 Total Cache'; } return $class; } return 'Unknown'; } // Orchestration. /** * Run. * * @param string $scope Scope. * @param bool $preview Preview. * @return array */ private function run( string $scope, bool $preview = false ): array { $results = array(); $append = function ( array $rows ) use ( &$results ) { foreach ( $rows as $r ) { $results[] = $r; } }; $do = function ( string $which ) use ( $preview ) { switch ( $which ) { case 'object_runtime': return array( $this->clear_object_runtime( $preview ) ); case 'object_full': return $this->clear_object_full( $preview ); case 'transients': return array( $this->clear_transients( $preview ) ); case 'rewrite': return array( $this->clear_rewrite( $preview ) ); case 'opcache': return array( $this->clear_opcache( $preview ) ); case 'page': return $this->clear_page_cache( $preview ); case 'builder': return $this->clear_builder_cache( $preview ); case 'wordpress_core': return $this->clear_wordpress_core( $preview ); case 'woocommerce': return $this->clear_woocommerce( $preview ); case 'browser_bust': return array( $this->clear_browser_bust( $preview ) ); } return array(); }; if ( 'all_safe' === $scope ) { $append( $do( 'object_runtime' ) ); $append( $do( 'transients' ) ); $append( $do( 'wordpress_core' ) ); if ( class_exists( 'WooCommerce' ) ) { $append( $do( 'woocommerce' ) ); } $append( $do( 'page' ) ); $append( $do( 'builder' ) ); $append( $do( 'browser_bust' ) ); return $results; } if ( 'all_aggressive' === $scope ) { $append( $do( 'object_runtime' ) ); $append( $do( 'object_full' ) ); $append( $do( 'transients' ) ); $append( $do( 'wordpress_core' ) ); if ( class_exists( 'WooCommerce' ) ) { $append( $do( 'woocommerce' ) ); } $append( $do( 'page' ) ); $append( $do( 'builder' ) ); $append( $do( 'rewrite' ) ); $append( $do( 'opcache' ) ); $append( $do( 'browser_bust' ) ); return $results; } $append( $do( $scope ) ); return $results; } /** * Summarize. * * @param array $results Results. * @return array */ private function summarize( array $results ): array { $c = array( 'executed' => 0, 'warning' => 0, 'not_detected' => 0, 'failed' => 0, 'skipped' => 0, ); foreach ( $results as $r ) { $s = $r['status'] ?? ''; if ( isset( $c[ $s ] ) ) { ++$c[ $s ]; } } $c['total'] = count( $results ); return $c; } // Log. /** * Append log. * * @param string $scope Scope. * @param string $mode Mode. * @param array $results Results. * @param array $summary Summary. */ private function append_log( string $scope, string $mode, array $results, array $summary ): void { $user = wp_get_current_user(); $trimmed = array_map( function ( $r ) { return array( 'label' => isset( $r['label'] ) ? (string) $r['label'] : '', 'status' => isset( $r['status'] ) ? (string) $r['status'] : '', 'detail' => isset( $r['detail'] ) ? (string) $r['detail'] : '', ); }, $results ); $entry = array( 'time' => time(), 'user_id' => $user ? (int) $user->ID : 0, 'user_login' => $user ? (string) $user->user_login : '', 'scope' => $scope, 'mode' => $mode, 'summary' => $summary, 'items' => $trimmed, ); $log = (array) get_option( self::LOG_OPTION, array() ); $log[] = $entry; if ( count( $log ) > self::LOG_LIMIT ) { $log = array_slice( $log, -self::LOG_LIMIT ); } update_option( self::LOG_OPTION, $log, false ); } // Object cache. /** * Clear object runtime. * * @param bool $preview Preview. * @return array */ private function clear_object_runtime( bool $preview = false ): array { if ( ! function_exists( 'wp_cache_flush_runtime' ) ) { return $this->row( __( 'Object cache (runtime)', 'ovanap-admin-toolkit' ), self::STATUS_NOT_DETECTED, __( 'wp_cache_flush_runtime() unavailable (requires WP 6.1+ or supporting drop-in)', 'ovanap-admin-toolkit' ) ); } if ( $preview ) { return $this->row( __( 'Object cache (runtime)', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would call wp_cache_flush_runtime()', 'ovanap-admin-toolkit' ) ); } $ok = false; try { $ok = (bool) wp_cache_flush_runtime(); } catch ( \Throwable $e ) { $ok = false; } return $ok ? $this->row( __( 'Object cache (runtime)', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'In-request cache flushed', 'ovanap-admin-toolkit' ) ) : $this->row( __( 'Object cache (runtime)', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, __( 'wp_cache_flush_runtime() refused', 'ovanap-admin-toolkit' ) ); } /** * Full flush — returns multiple rows so the persistent/multisite warning * is surfaced alongside the actual flush result. * * @param bool $preview Preview. * @return array */ private function clear_object_full( bool $preview = false ): array { $rows = array(); $env = $this->get_environment(); if ( $env['persistent_object_cache'] ) { /* translators: %s: object cache backend name */ $rows[] = $this->row( __( 'Shared backend warning', 'ovanap-admin-toolkit' ), self::STATUS_WARNING, /* translators: %s: object cache backend name */ sprintf( __( 'Persistent object cache (%s) — flush may affect other sites sharing this backend', 'ovanap-admin-toolkit' ), $env['object_cache_backend'] ) ); } if ( $env['multisite'] ) { $rows[] = $this->row( __( 'Multisite warning', 'ovanap-admin-toolkit' ), self::STATUS_WARNING, __( 'Network-wide flush affects every site on this install', 'ovanap-admin-toolkit' ) ); } if ( ! function_exists( 'wp_cache_flush' ) ) { $rows[] = $this->row( __( 'Object cache (full)', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, __( 'wp_cache_flush() unavailable', 'ovanap-admin-toolkit' ) ); return $rows; } if ( $preview ) { $rows[] = $this->row( __( 'Object cache (full)', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would call wp_cache_flush()', 'ovanap-admin-toolkit' ) ); return $rows; } $ok = false; try { $ok = (bool) wp_cache_flush(); } catch ( \Throwable $e ) { $ok = false; } $rows[] = $ok ? $this->row( __( 'Object cache (full)', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'All entries removed from configured backend', 'ovanap-admin-toolkit' ) ) : $this->row( __( 'Object cache (full)', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, __( 'wp_cache_flush() refused', 'ovanap-admin-toolkit' ) ); return $rows; } // Transients. /** * Clear transients. * * @param bool $preview Preview. * @return array */ private function clear_transients( bool $preview = false ): array { global $wpdb; // An administrator explicitly asked to clear every transient. There is no // core API for "all transients", and caching these counts would defeat the purpose. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_%' OR option_name LIKE '\\_site\\_transient\\_%'" ); if ( is_multisite() && isset( $wpdb->sitemeta ) ) { $count += (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->sitemeta} WHERE meta_key LIKE '\\_site\\_transient\\_%'" ); } if ( $preview ) { return $this->row( __( 'Transients', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, /* translators: %d: rows that would be removed */ sprintf( _n( '%d row would be removed', '%d rows would be removed', $count, 'ovanap-admin-toolkit' ), $count ) ); } $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_%' OR option_name LIKE '\\_site\\_transient\\_%'" ); if ( is_multisite() && isset( $wpdb->sitemeta ) ) { $wpdb->query( "DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE '\\_site\\_transient\\_%'" ); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( function_exists( 'wp_cache_delete' ) ) { wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); } return $this->row( __( 'Transients', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, /* translators: %d: number of transient rows removed */ sprintf( _n( '%d row removed', '%d rows removed', $count, 'ovanap-admin-toolkit' ), $count ) ); } // Rewrite / OPcache. /** * Clear rewrite. * * @param bool $preview Preview. * @return array */ private function clear_rewrite( bool $preview = false ): array { if ( $preview ) { return $this->row( __( 'Rewrite rules', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would soft-flush (no .htaccess write)', 'ovanap-admin-toolkit' ) ); } flush_rewrite_rules( false ); return $this->row( __( 'Rewrite rules', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Flushed (soft)', 'ovanap-admin-toolkit' ) ); } /** * Clear opcache. * * @param bool $preview Preview. * @return array */ private function clear_opcache( bool $preview = false ): array { if ( ! function_exists( 'opcache_reset' ) ) { return $this->row( __( 'OPcache', 'ovanap-admin-toolkit' ), self::STATUS_NOT_DETECTED, __( 'opcache_reset() unavailable on this server', 'ovanap-admin-toolkit' ) ); } if ( $preview ) { return $this->row( __( 'OPcache', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would call opcache_reset()', 'ovanap-admin-toolkit' ) ); } $ok = @opcache_reset(); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- opcache.restrict_api makes this emit a warning; failure is reported in the result row. return $ok ? $this->row( __( 'OPcache', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Reset', 'ovanap-admin-toolkit' ) ) : $this->row( __( 'OPcache', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, __( 'Refused (opcache.restrict_api?)', 'ovanap-admin-toolkit' ) ); } // WordPress core extras. /** * Clear WordPress core. * * @param bool $preview Preview. * @return array */ private function clear_wordpress_core( bool $preview = false ): array { $out = array(); $mf = ABSPATH . '.maintenance'; if ( file_exists( $mf ) ) { if ( $preview ) { $out[] = $this->row( __( '.maintenance file', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would remove stuck maintenance file', 'ovanap-admin-toolkit' ) ); } else { $removed = false; try { wp_delete_file( $mf ); $removed = ! file_exists( $mf ); } catch ( \Throwable $e ) { $removed = false; } $out[] = $removed ? $this->row( __( '.maintenance file', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Removed', 'ovanap-admin-toolkit' ) ) : $this->row( __( '.maintenance file', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, __( 'Could not remove (filesystem permissions?)', 'ovanap-admin-toolkit' ) ); } } if ( get_site_transient( 'core_updater.lock' ) ) { if ( ! $preview ) { delete_site_transient( 'core_updater.lock' ); } $out[] = $this->row( __( 'Core updater lock', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, $preview ? __( 'Would release', 'ovanap-admin-toolkit' ) : __( 'Released', 'ovanap-admin-toolkit' ) ); } $update_keys = array( 'update_core', 'update_plugins', 'update_themes' ); $released = array(); foreach ( $update_keys as $k ) { if ( get_site_transient( $k ) !== false ) { if ( ! $preview ) { delete_site_transient( $k ); } $released[] = $k; } } if ( ! empty( $released ) ) { $out[] = $this->row( __( 'Update check cache', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, ( $preview ? __( 'Would invalidate: ', 'ovanap-admin-toolkit' ) : __( 'Invalidated: ', 'ovanap-admin-toolkit' ) ) . implode( ', ', $released ) ); } if ( get_transient( 'health-check-site-status-result' ) !== false ) { if ( ! $preview ) { delete_transient( 'health-check-site-status-result' ); } $out[] = $this->row( __( 'Site Health status', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, $preview ? __( 'Would invalidate', 'ovanap-admin-toolkit' ) : __( 'Invalidated', 'ovanap-admin-toolkit' ) ); } if ( class_exists( 'SimplePie_Cache' ) && method_exists( 'SimplePie_Cache', 'flush' ) ) { if ( ! $preview ) { try { \SimplePie_Cache::flush(); } catch ( \Throwable $e ) { // Best effort: the row below still reports the attempt. unset( $e ); } } $out[] = $this->row( __( 'SimplePie feed cache', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, $preview ? __( 'Would flush', 'ovanap-admin-toolkit' ) : __( 'Flushed', 'ovanap-admin-toolkit' ) ); } $cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-7 days' ) ); $orphan_ids = get_posts( array( 'post_type' => 'customize_changeset', 'post_status' => array( 'auto-draft', 'trash' ), 'posts_per_page' => 50, 'fields' => 'ids', 'date_query' => array( array( 'before' => $cutoff, 'column' => 'post_modified_gmt', ), ), 'no_found_rows' => true, ) ); $orphan_count = is_array( $orphan_ids ) ? count( $orphan_ids ) : 0; if ( $orphan_count > 0 ) { if ( ! $preview ) { foreach ( $orphan_ids as $id ) { wp_delete_post( (int) $id, true ); } } $out[] = $this->row( __( 'Customizer drafts', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, sprintf( $preview /* translators: %d: number of stale customizer changesets */ ? _n( '%d stale changeset would be removed', '%d stale changesets would be removed', $orphan_count, 'ovanap-admin-toolkit' ) /* translators: %d: number of stale customizer changesets */ : _n( '%d stale changeset removed', '%d stale changesets removed', $orphan_count, 'ovanap-admin-toolkit' ), $orphan_count ) ); } if ( empty( $out ) ) { $out[] = $this->row( __( 'WordPress core caches', 'ovanap-admin-toolkit' ), self::STATUS_NOT_DETECTED, __( 'No stale core state detected', 'ovanap-admin-toolkit' ) ); } return apply_filters( 'ovkit_cache_manager_wordpress_core_results', $out, $preview ); } // WooCommerce. /** * Clear woocommerce. * * @param bool $preview Preview. * @return array */ private function clear_woocommerce( bool $preview = false ): array { $out = array(); if ( ! class_exists( 'WooCommerce' ) ) { $out[] = $this->row( 'WooCommerce', self::STATUS_NOT_DETECTED, __( 'Not active', 'ovanap-admin-toolkit' ) ); return $out; } if ( class_exists( 'WC_Cache_Helper' ) && method_exists( 'WC_Cache_Helper', 'get_transient_version' ) ) { if ( $preview ) { $out[] = $this->row( __( 'WC product transient version', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would bump', 'ovanap-admin-toolkit' ) ); $out[] = $this->row( __( 'WC order transient version', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would bump', 'ovanap-admin-toolkit' ) ); } else { try { \WC_Cache_Helper::get_transient_version( 'product', true ); $out[] = $this->row( __( 'WC product transient version', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Bumped (compatibility layer)', 'ovanap-admin-toolkit' ) ); } catch ( \Throwable $e ) { $out[] = $this->row( __( 'WC product transient version', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, $e->getMessage() ); } try { \WC_Cache_Helper::get_transient_version( 'orders', true ); $out[] = $this->row( __( 'WC order transient version', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Bumped (compatibility layer)', 'ovanap-admin-toolkit' ) ); } catch ( \Throwable $e ) { $out[] = $this->row( __( 'WC order transient version', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, $e->getMessage() ); } } } if ( get_transient( 'wc_count_comments' ) !== false ) { if ( ! $preview ) { delete_transient( 'wc_count_comments' ); } $out[] = $this->row( __( 'WC review counts', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, $preview ? __( 'Would invalidate', 'ovanap-admin-toolkit' ) : __( 'Invalidated', 'ovanap-admin-toolkit' ) ); } if ( function_exists( 'wc_delete_expired_transients' ) ) { if ( $preview ) { $out[] = $this->row( __( 'WC expired transients', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Would run expired-transient cleanup', 'ovanap-admin-toolkit' ) ); } else { try { wc_delete_expired_transients(); $out[] = $this->row( __( 'WC expired transients', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, __( 'Cleanup ran', 'ovanap-admin-toolkit' ) ); } catch ( \Throwable $e ) { $out[] = $this->row( __( 'WC expired transients', 'ovanap-admin-toolkit' ), self::STATUS_FAILED, $e->getMessage() ); } } } return apply_filters( 'ovkit_cache_manager_woocommerce_results', $out, $preview ); } // Browser bust. /** * Clear browser bust. * * @param bool $preview Preview. * @return array */ private function clear_browser_bust( bool $preview = false ): array { $current = (int) get_option( self::BUST_OPTION, 0 ); if ( $preview ) { return $this->row( __( 'Browser asset bust', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, $current ? __( 'Would bump ?ovkbust version on same-origin assets', 'ovanap-admin-toolkit' ) : __( 'Would initialise ?ovkbust version', 'ovanap-admin-toolkit' ) ); } $new = time(); update_option( self::BUST_OPTION, $new, false ); $this->bust_version = $new; /* translators: %s: version timestamp */ return $this->row( __( 'Browser asset bust', 'ovanap-admin-toolkit' ), self::STATUS_EXECUTED, sprintf( __( 'Version set to %s', 'ovanap-admin-toolkit' ), (string) $new ) ); } /** * Purge page caches after the toolkit's settings change, so front-end switches * (announcement bar, tracking, head cleanup) show up at once on cached sites. * Runs without booting the admin-bar tool and is not written to its log. * * @return string[] Names of the page caches that were purged. */ public function purge_pages_after_settings_change(): array { $names = array(); foreach ( $this->clear_page_cache( false ) as $row ) { if ( self::STATUS_EXECUTED === ( $row['status'] ?? '' ) ) { $names[] = (string) $row['label']; } } return $names; } // Page cache vendors. /** * Clear page cache. * * @param bool $preview Preview. * @return array */ private function clear_page_cache( bool $preview = false ): array { $out = array(); $try = function ( string $label, callable $probe, callable $action, string $detail_ok ) use ( &$out, $preview ) { if ( ! $probe() ) { return; } if ( $preview ) { $out[] = $this->row( $label, self::STATUS_EXECUTED, __( 'Would purge', 'ovanap-admin-toolkit' ) ); return; } try { $action(); $out[] = $this->row( $label, self::STATUS_EXECUTED, $detail_ok ); } catch ( \Throwable $e ) { $out[] = $this->row( $label, self::STATUS_FAILED, $e->getMessage() ); } }; $try( 'WP Rocket', fn() => function_exists( 'rocket_clean_domain' ), function () { rocket_clean_domain(); if ( function_exists( 'rocket_clean_minify' ) ) { rocket_clean_minify(); } if ( function_exists( 'rocket_clean_cache_busting' ) ) { rocket_clean_cache_busting(); } }, __( 'Domain + minify cache cleaned', 'ovanap-admin-toolkit' ) ); $try( 'W3 Total Cache', fn() => function_exists( 'w3tc_flush_all' ) || function_exists( 'w3tc_pgcache_flush' ), function () { if ( function_exists( 'w3tc_flush_all' ) ) { w3tc_flush_all(); } else { w3tc_pgcache_flush(); } }, __( 'Flushed', 'ovanap-admin-toolkit' ) ); $try( 'WP Super Cache', fn() => function_exists( 'wp_cache_clear_cache' ), fn() => wp_cache_clear_cache(), __( 'Cache cleared', 'ovanap-admin-toolkit' ) ); $try( 'LiteSpeed Cache', fn() => class_exists( '\LiteSpeed\Purge' ) || has_action( 'litespeed_purge_all' ), fn() => do_action( 'litespeed_purge_all' ), __( 'Purge signal fired', 'ovanap-admin-toolkit' ) ); $server = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; if ( stripos( $server, 'litespeed' ) !== false && ! headers_sent() ) { if ( $preview ) { $out[] = $this->row( 'LiteSpeed server', self::STATUS_EXECUTED, __( 'Would send X-LiteSpeed-Purge header', 'ovanap-admin-toolkit' ) ); } else { header( 'X-LiteSpeed-Purge: *' ); $out[] = $this->row( 'LiteSpeed server', self::STATUS_EXECUTED, __( 'Sent X-LiteSpeed-Purge: *', 'ovanap-admin-toolkit' ) ); } } $try( 'SG Optimizer', fn() => function_exists( 'sg_cachepress_purge_cache' ), fn() => sg_cachepress_purge_cache(), __( 'Cache purged', 'ovanap-admin-toolkit' ) ); $try( 'Hummingbird', fn() => has_action( 'wphb_clear_page_cache' ), fn() => do_action( 'wphb_clear_page_cache' ), __( 'Page cache cleared', 'ovanap-admin-toolkit' ) ); $try( 'Cache Enabler', fn() => class_exists( 'Cache_Enabler' ) && method_exists( 'Cache_Enabler', 'clear_complete_cache' ), fn() => \Cache_Enabler::clear_complete_cache(), __( 'Cache cleared', 'ovanap-admin-toolkit' ) ); $try( 'WP Fastest Cache', fn() => class_exists( 'WpFastestCache' ), function () { $wpfc = new \WpFastestCache(); if ( method_exists( $wpfc, 'deleteCache' ) ) { $wpfc->deleteCache( true ); } }, __( 'Cache deleted', 'ovanap-admin-toolkit' ) ); if ( empty( $out ) ) { $out[] = $this->row( __( 'Page cache', 'ovanap-admin-toolkit' ), self::STATUS_NOT_DETECTED, __( 'No supported page cache plugin detected', 'ovanap-admin-toolkit' ) ); } return apply_filters( 'ovkit_cache_manager_page_cache_results', $out, $preview ); } // Builder cache. /** * Clear builder cache. * * @param bool $preview Preview. * @return array */ private function clear_builder_cache( bool $preview = false ): array { $out = array(); if ( class_exists( '\Elementor\Plugin' ) ) { $el = \Elementor\Plugin::$instance ?? null; if ( $el && isset( $el->files_manager ) && method_exists( $el->files_manager, 'clear_cache' ) ) { if ( $preview ) { $out[] = $this->row( 'Elementor', self::STATUS_EXECUTED, __( 'Would clear generated CSS', 'ovanap-admin-toolkit' ) ); } else { try { $el->files_manager->clear_cache(); $out[] = $this->row( 'Elementor', self::STATUS_EXECUTED, __( 'Generated CSS cleared', 'ovanap-admin-toolkit' ) ); } catch ( \Throwable $e ) { $out[] = $this->row( 'Elementor', self::STATUS_FAILED, $e->getMessage() ); } } } } $gb_class = class_exists( 'GenerateBlocks_Dynamic_CSS' ) && method_exists( 'GenerateBlocks_Dynamic_CSS', 'flush' ); $gb_opt = get_option( 'generateblocks_dynamic_css_version' ) !== false; if ( $gb_class || $gb_opt ) { if ( $preview ) { $out[] = $this->row( 'GenerateBlocks', self::STATUS_EXECUTED, __( 'Would flush dynamic CSS', 'ovanap-admin-toolkit' ) ); } else { try { if ( $gb_class ) { \GenerateBlocks_Dynamic_CSS::flush(); $out[] = $this->row( 'GenerateBlocks', self::STATUS_EXECUTED, __( 'Dynamic CSS flushed', 'ovanap-admin-toolkit' ) ); } else { update_option( 'generateblocks_dynamic_css_version', time() ); $out[] = $this->row( 'GenerateBlocks', self::STATUS_EXECUTED, __( 'Dynamic CSS version bumped', 'ovanap-admin-toolkit' ) ); } } catch ( \Throwable $e ) { $out[] = $this->row( 'GenerateBlocks', self::STATUS_FAILED, $e->getMessage() ); } } } if ( class_exists( 'autoptimizeCache' ) && method_exists( 'autoptimizeCache', 'clearall' ) ) { if ( $preview ) { $out[] = $this->row( 'Autoptimize', self::STATUS_EXECUTED, __( 'Would clear all caches', 'ovanap-admin-toolkit' ) ); } else { try { \autoptimizeCache::clearall(); $out[] = $this->row( 'Autoptimize', self::STATUS_EXECUTED, __( 'All caches cleared', 'ovanap-admin-toolkit' ) ); } catch ( \Throwable $e ) { $out[] = $this->row( 'Autoptimize', self::STATUS_FAILED, $e->getMessage() ); } } } if ( empty( $out ) ) { $out[] = $this->row( __( 'Builder / asset cache', 'ovanap-admin-toolkit' ), self::STATUS_NOT_DETECTED, __( 'No supported builder/asset cache detected', 'ovanap-admin-toolkit' ) ); } return apply_filters( 'ovkit_cache_manager_builder_cache_results', $out, $preview ); } // Detection. /** * Detect systems. * * @return array */ public function detect_systems(): array { $found = array(); if ( function_exists( 'rocket_clean_domain' ) ) { $found[] = array( 'key' => 'wp_rocket', 'label' => 'WP Rocket', 'scopes' => array( 'page' ), ); } if ( function_exists( 'w3tc_flush_all' ) || function_exists( 'w3tc_pgcache_flush' ) ) { $found[] = array( 'key' => 'w3tc', 'label' => 'W3 Total Cache', 'scopes' => array( 'page' ), ); } if ( function_exists( 'wp_cache_clear_cache' ) ) { $found[] = array( 'key' => 'wp_super_cache', 'label' => 'WP Super Cache', 'scopes' => array( 'page' ), ); } if ( class_exists( '\LiteSpeed\Purge' ) || has_action( 'litespeed_purge_all' ) ) { $found[] = array( 'key' => 'litespeed', 'label' => 'LiteSpeed Cache', 'scopes' => array( 'page' ), ); } if ( function_exists( 'sg_cachepress_purge_cache' ) ) { $found[] = array( 'key' => 'sg_optimizer', 'label' => 'SG Optimizer', 'scopes' => array( 'page' ), ); } if ( has_action( 'wphb_clear_page_cache' ) ) { $found[] = array( 'key' => 'hummingbird', 'label' => 'Hummingbird', 'scopes' => array( 'page' ), ); } if ( class_exists( 'Cache_Enabler' ) ) { $found[] = array( 'key' => 'cache_enabler', 'label' => 'Cache Enabler', 'scopes' => array( 'page' ), ); } if ( class_exists( 'WpFastestCache' ) ) { $found[] = array( 'key' => 'wp_fastest', 'label' => 'WP Fastest Cache', 'scopes' => array( 'page' ), ); } if ( class_exists( '\Elementor\Plugin' ) ) { $found[] = array( 'key' => 'elementor', 'label' => 'Elementor', 'scopes' => array( 'builder' ), ); } if ( class_exists( 'GenerateBlocks_Dynamic_CSS' ) || get_option( 'generateblocks_dynamic_css_version' ) !== false ) { $found[] = array( 'key' => 'generateblocks', 'label' => 'GenerateBlocks', 'scopes' => array( 'builder' ), ); } if ( class_exists( 'autoptimizeCache' ) ) { $found[] = array( 'key' => 'autoptimize', 'label' => 'Autoptimize', 'scopes' => array( 'builder' ), ); } if ( class_exists( 'WooCommerce' ) ) { $found[] = array( 'key' => 'woocommerce', 'label' => 'WooCommerce', 'scopes' => array( 'woocommerce' ), ); } $server = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; if ( stripos( $server, 'litespeed' ) !== false ) { $found[] = array( 'key' => 'litespeed_srv', 'label' => 'LiteSpeed server', 'scopes' => array( 'page' ), ); } return $found; } }