outer = true; if ( self::wp_buffer() ) { // WordPress 6.9+: the template output buffer of WordPress hands // over the finished page, no buffer of our own. add_action( 'wp_finalized_template_enhancement_output_buffer', array( $this, 'store_final' ) ); } else { ob_start( array( $this, 'outer_buffer' ) ); add_action( 'shutdown', array( $this, 'close_buffers' ), 0 ); } } } // Critical CSS analysis from the admin panel: page without the admin bar. if ( isset( $_GET['ctps_critical'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification add_filter( 'show_admin_bar', '__return_false' ); } // .htaccess rules to write (requested by WP-CLI). if ( get_option( 'ctps_htaccess_pending' ) && ! ( defined( 'WP_CLI' ) && WP_CLI ) ) { add_action( 'init', function () { delete_option( 'ctps_htaccess_pending' ); if ( CTPS_Settings::on( 'htaccess_rules' ) ) { CTPS_Htaccess::write(); } } ); } // Re-enabled after an emergency (ctps-off file removed, constant removed). if ( get_option( 'ctps_disabled_cleaned' ) ) { add_action( 'init', function () { delete_option( 'ctps_disabled_cleaned' ); if ( CTPS_Settings::on( 'htaccess_rules' ) ) { CTPS_Htaccess::write(); } } ); } if ( get_option( 'ctps_version' ) !== CTPS_VERSION ) { add_action( 'init', array( __CLASS__, 'upgrade' ) ); } CTPS_Cleanup::init(); CTPS_Media::init(); CTPS_Front_Assets::init(); CTPS_Fonts::init(); CTPS_Webp::init(); CTPS_Webp_Replace::init(); CTPS_Headers::init(); CTPS_Preload::init(); CTPS_PSI::init(); CTPS_RUM::init(); CTPS_Report::init(); CTPS_Alerts::init(); CTPS_Asset_Manager::init(); CTPS_Used_CSS::init(); CTPS_CDN::init(); CTPS_Hosting::init(); CTPS_Object_Cache::init(); CTPS_Privacy::init(); $this->prefetch_links(); if ( is_admin() ) { CTPS_Admin::init(); CTPS_Post_Meta::init(); CTPS_Wizard::init(); } add_action( 'template_redirect', array( $this, 'start_fallback' ), 0 ); add_action( 'template_redirect', array( $this, 'start_buffer' ), PHP_INT_MAX ); add_action( 'ctps_purge_expired', array( 'CTPS_Page_Cache', 'purge_expired' ) ); add_action( 'update_option_' . CTPS_Settings::OPTION, array( $this, 'on_settings_saved' ), 10, 0 ); // Purges the cache when content changes. // A piece of content changes: only the related pages are purged. // Publish/unpublish (status change to or from 'publish'): add_action( 'transition_post_status', array( $this, 'purge_on_transition' ), 99, 3 ); // Update of an already published post (drafts, pending, scheduled: nothing to purge): add_action( 'save_post', array( $this, 'purge_post' ), 99 ); // Before deleting/trashing: afterwards, permalink and type are no longer the public ones. add_action( 'before_delete_post', array( $this, 'purge_post' ), 99 ); add_action( 'wp_trash_post', array( $this, 'purge_post' ), 99 ); add_action( 'post_updated', array( $this, 'purge_old_url' ), 99, 3 ); add_action( 'woocommerce_product_set_stock', array( $this, 'purge_product' ), 99 ); foreach ( array( 'comment_post', 'edit_comment', 'wp_set_comment_status' ) as $hook ) { add_action( $hook, array( $this, 'purge_comment' ), 99 ); } // Something that appears on every page changes: purge everything. foreach ( array( 'edit_term', 'delete_term', 'switch_theme', 'wp_update_nav_menu', 'customize_save_after', 'activated_plugin', 'deactivated_plugin', 'upgrader_process_complete', 'update_option_sidebars_widgets' ) as $hook ) { add_action( $hook, array( $this, 'purge_on_change' ), 99 ); } } /** @var array Post IDs already purged in this request. */ private $purged = array(); /** * Purges the pages of a published post. Drafts, pending, scheduled and * private posts are not public: editing them leaves the cache alone. */ public function purge_post( $post_id ) { if ( 'publish' !== get_post_status( $post_id ) ) { return; } $this->purge_post_pages( $post_id ); } /** * Purge only when a status change makes a post public or removes it. */ public static function transition_purges( $new_status, $old_status ) { return $new_status !== $old_status && ( 'publish' === $new_status || 'publish' === $old_status ); } public function purge_on_transition( $new_status, $old_status, $post ) { // Updates of an already published post (publish → publish) are handled by save_post. if ( ! $post instanceof WP_Post || ! self::transition_purges( $new_status, $old_status ) ) { return; } $this->purge_post_pages( $post->ID ); } private function purge_post_pages( $post_id ) { $post_id = (int) $post_id; if ( ! $post_id || isset( $this->purged[ $post_id ] ) || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } $type = get_post_type( $post_id ); if ( ! $type || ! is_post_type_viewable( $type ) || 'attachment' === $type ) { return; } $this->purged[ $post_id ] = true; $urls = CTPS_Page_Cache::purge_post( $post_id ); CTPS_Preload::enqueue( $urls ); } /** * Address change or page unpublished: the old URL is purged. */ public function purge_old_url( $post_id, $after, $before ) { if ( ! $before instanceof WP_Post || 'publish' !== $before->post_status ) { return; } if ( $before->post_name !== $after->post_name || 'publish' !== $after->post_status || $before->post_parent !== $after->post_parent ) { $old = get_permalink( $before ); if ( $old ) { CTPS_Page_Cache::purge_url( $old ); do_action( 'ctps_urls_purged', array( $old ) ); } } } public function purge_product( $product ) { if ( is_object( $product ) && method_exists( $product, 'get_id' ) ) { $this->purge_post( $product->get_id() ); } } public function purge_comment( $comment_id ) { $comment = get_comment( $comment_id ); if ( $comment && $comment->comment_post_ID ) { $this->purge_post( (int) $comment->comment_post_ID ); } } public function purge_on_change( $maybe_post_id = null ) { $hook = current_filter(); if ( in_array( $hook, array( 'activated_plugin', 'deactivated_plugin' ), true ) ) { // This plugin itself: deactivate() already removed the cache, and a // purge here would schedule the preload cron again. if ( plugin_basename( WPADAPT_FILE ) === $maybe_post_id ) { return; } // A mobile-theme plugin changes the mobile cache variant: rewrite the // server rules and the early cache config at the end of the request, // when the active plugins list has been saved. if ( CTPS_Page_Cache::is_mobile_plugin( $maybe_post_id ) && ! has_action( 'shutdown', array( $this, 'refresh_server_rules' ) ) ) { add_action( 'shutdown', array( $this, 'refresh_server_rules' ) ); } } if ( is_numeric( $maybe_post_id ) && in_array( current_filter(), array( 'save_post', 'deleted_post', 'trashed_post' ), true ) ) { if ( wp_is_post_revision( $maybe_post_id ) || wp_is_post_autosave( $maybe_post_id ) ) { return; } } CTPS_Page_Cache::purge_all(); } public function refresh_server_rules() { CTPS_Settings::flush_cache(); if ( CTPS_Settings::on( 'htaccess_rules' ) && WPADAPT_Policy::can( 'htaccess_rules' ) ) { CTPS_Htaccess::write(); } if ( class_exists( 'WPADAPT_Early_Cache' ) ) { WPADAPT_Early_Cache::write_config(); } } public function on_settings_saved() { CTPS_Settings::flush_cache(); CTPS_Page_Cache::purge_all(); if ( CTPS_Settings::on( 'htaccess_rules' ) && WPADAPT_Policy::can( 'htaccess_rules' ) ) { CTPS_Htaccess::write(); } else { CTPS_Htaccess::remove(); } // Checks that the site still works; otherwise reverts to the previous snapshot. CTPS_Safety::health_check(); } public function should_optimize() { if ( class_exists( 'WPADAPT_Adaptive_Engine' ) && WPADAPT_Adaptive_Engine::should_bypass_output_optimization() ) { return false; } if ( is_admin() || is_feed() || is_embed() || is_robots() || is_trackback() || is_customize_preview() ) { return false; } // Logged-in users (editors, page builders, customers) get the original // HTML: lower overhead (measured ~+15 ms per logged-in view) and no risk // of rewritten scripts breaking editing tools. Visitors are unaffected. if ( is_user_logged_in() && ! apply_filters( 'ctps_optimize_logged_in', false ) ) { return false; } if ( wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) { return false; } if ( isset( $_GET['ctps_off'] ) || isset( $_GET['ctps_assets'] ) || isset( $_GET['elementor-preview'] ) || isset( $_GET['fl_builder'] ) || isset( $_GET['et_fb'] ) || isset( $_GET['ct_builder'] ) || isset( $_GET['bricks'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return false; } if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) { return false; } if ( CTPS_Post_Meta::current( '_ctps_off' ) ) { return false; } return (bool) apply_filters( 'ctps_should_optimize', true ); } /** @var bool The WordPress template buffer optimized this page. */ private $enhanced = false; /** * Fallback for pages the WordPress template buffer never sees (3.7.29). * * The WordPress template buffer (6.9+) is opened only when the template * loader includes a template and nothing turns it off: some themes and * plugins print the page and exit earlier, or keep it from starting, and * those pages were never optimized. This outer buffer, opened early on * template_redirect, optimizes the page only when the WordPress buffer did * not; otherwise it passes the page through unchanged, so pages optimized * by the WordPress buffer stay exactly as in 3.7.26. */ public function start_fallback() { if ( ! self::wp_buffer() || ! $this->should_optimize() ) { return; } ob_start( array( $this, 'fallback_buffer' ) ); add_action( 'shutdown', array( $this, 'close_buffers' ), 0 ); } public function fallback_buffer( $html, $phase = PHP_OUTPUT_HANDLER_FINAL ) { if ( $this->enhanced ) { // Parts held back by an earlier flush are sent unchanged too. $out = $this->chunks . $html; $this->chunks = ''; return $out; } if ( ( $phase & PHP_OUTPUT_HANDLER_FINAL ) && CTPS_Log::enabled() ) { CTPS_Log::add( 'optimize', 'WordPress template buffer did not run: fallback buffer used' ); } return $this->end_buffer( $html, $phase ); } public function start_buffer() { if ( ! $this->should_optimize() ) { return; } if ( CTPS_Log::enabled() ) { CTPS_Log::add( 'optimize', self::wp_buffer() ? 'Buffer started (WordPress template buffer)' : 'Buffer started' ); } if ( self::wp_buffer() ) { add_filter( 'wp_template_enhancement_output_buffer', array( $this, 'enhance' ) ); return; } ob_start( array( $this, 'end_buffer' ) ); add_action( 'shutdown', array( $this, 'close_buffers' ), 0 ); } /** WordPress 6.9+ provides the template enhancement output buffer. */ public static function wp_buffer() { return function_exists( 'wp_start_template_enhancement_output_buffer' ) && (bool) apply_filters( 'ctps_use_template_buffer', true ); } /** Filter of the WordPress template output buffer (6.9+): the whole page. */ public function enhance( $html ) { $this->enhanced = true; return ( is_string( $html ) && '' !== $html ) ? $this->optimize_page( $html ) : $html; } /** Action after every template enhancement (6.9+): the page as sent, stored in the cache. */ public function store_final( $html ) { if ( ! $this->store_ok || ! is_string( $html ) || '' !== CTPS_Page_Cache::not_cacheable_reason( $html ) ) { return; } $this->store_ok = false; try { CTPS_Page_Cache::store( $html ); } catch ( \Throwable $e ) { CTPS_Log::error( $e, 'Page cache store' ); } } /** * Before WordPress 6.9: the buffers opened by this plugin are closed here, * on shutdown, before WordPress flushes the others. A buffer of another * plugin opened later stays on top and is left to WordPress. */ public function close_buffers() { $mine = array( __CLASS__ . '::end_buffer', __CLASS__ . '::outer_buffer', __CLASS__ . '::fallback_buffer' ); while ( ob_get_level() > 0 ) { $status = ob_get_status(); if ( empty( $status['name'] ) || ! in_array( $status['name'], $mine, true ) ) { break; } ob_end_flush(); } } /** @var string Parts of the page already sent by ob_flush(). */ private $chunks = ''; /** @var bool Outer capture buffer active for this request. */ private $outer = false; /** @var bool The inner pass decided this page may be cached. */ private $store_ok = false; /** @var string Chunks for the outer buffer. */ private $outer_chunks = ''; public function outer_buffer( $html, $phase = PHP_OUTPUT_HANDLER_FINAL ) { // ob_clean()/ob_end_clean(): $html is discarded output, never part of the page. if ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) { if ( $phase & PHP_OUTPUT_HANDLER_FINAL ) { $this->outer_chunks = ''; $this->store_ok = false; } return ''; } if ( ! ( $phase & PHP_OUTPUT_HANDLER_FINAL ) ) { $this->outer_chunks .= $html; return $html; } $final = $this->outer_chunks . $html; $this->outer_chunks = ''; if ( $this->store_ok && '' === CTPS_Page_Cache::not_cacheable_reason( $final ) ) { try { CTPS_Page_Cache::store( $final ); } catch ( \Throwable $e ) { CTPS_Log::error( $e, 'Page cache store' ); } } return $html; } public function end_buffer( $html, $phase = PHP_OUTPUT_HANDLER_FINAL ) { // ob_clean()/ob_end_clean(): $html is discarded output. Chunks held back // from earlier ob_flush() calls stay (they were meant to be sent); after // ob_end_clean() nothing is optimized or cached. if ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) { if ( $phase & PHP_OUTPUT_HANDLER_FINAL ) { $this->chunks = ''; } return ''; } // If someone calls ob_flush() chunks arrive: only the whole page is optimized. if ( ! ( $phase & PHP_OUTPUT_HANDLER_FINAL ) ) { $this->chunks .= $html; return ''; } $html = $this->chunks . $html; $this->chunks = ''; return $this->optimize_page( $html ); } private function optimize_page( $html ) { // Transformations of resource URLs, before the HTML optimization // (so preload and preconnect already point to the final versions). $pre = array(); if ( CTPS_Settings::on( 'conditional_assets' ) && WPADAPT_Policy::can( 'plugin_assets' ) ) { $pre[] = array( 'WPADAPT_Plugin_Assets', 'process' ); // First: works on the original WordPress tag ids. } if ( CTPS_Settings::on( 'turnstile_lazy' ) ) { $pre[] = array( 'WPADAPT_Turnstile_Lazy', 'process' ); } if ( CTPS_Settings::on( 'local_google_fonts' ) ) { $pre[] = array( 'CTPS_Fonts', 'process' ); } if ( CTPS_Webp::serve_on() || CTPS_Webp::avif_on() ) { $pre[] = array( 'CTPS_Webp', 'process' ); } if ( CTPS_Settings::on( 'used_css' ) && WPADAPT_Policy::can( 'used_css' ) ) { $pre[] = array( 'CTPS_Used_CSS', 'process' ); // Before minification: compares the original URLs. } if ( CTPS_Settings::on( 'minify_css' ) && WPADAPT_Policy::can( 'css_minify' ) ) { $pre[] = array( 'CTPS_CSS_Minify', 'process' ); } if ( CTPS_Settings::on( 'js_minify' ) && WPADAPT_Policy::can( 'js_minify' ) ) { $pre[] = array( 'CTPS_JS_Minify', 'process' ); } if ( '' !== (string) CTPS_Settings::get( 'cdn_url' ) ) { $pre[] = array( 'CTPS_CDN', 'process' ); // Last: also applies to WebP, fonts and generated CSS. } if ( false !== stripos( $html, '' ) ) { foreach ( $pre as $cb ) { try { $out = call_user_func( $cb, $html ); if ( is_string( $out ) && '' !== $out ) { $html = $out; } } catch ( \Throwable $e ) { CTPS_Log::error( $e, implode( '::', $cb ) ); } } } $host = wp_parse_url( home_url(), PHP_URL_HOST ); $options = CTPS_Compat::apply( CTPS_Settings::all() ); $options = WPADAPT_Policy::apply_options( $options ); $started = microtime( true ); if ( CTPS_Post_Meta::current( '_ctps_no_js' ) ) { $options['defer_js'] = 0; $options['delay_js'] = 0; } if ( CTPS_Post_Meta::current( '_ctps_no_lazy' ) ) { $options['lazy_load'] = 0; $options['lazy_bg'] = 0; $options['lazy_iframes'] = 0; } $optimizer = new CTPS_HTML_Optimizer( $options, $host, CTPS_Settings::on( 'add_dimensions' ) ? array( 'CTPS_Media', 'resolve_dimensions' ) : null ); try { $optimized = $optimizer->process( $html ); } catch ( \Throwable $e ) { CTPS_Log::error( $e, 'HTML optimizer' ); return $html; // Never break the page. } if ( ! is_string( $optimized ) || '' === $optimized ) { return $html; } $reason = CTPS_Page_Cache::not_cacheable_reason( $optimized ); if ( ! WPADAPT_Policy::can( 'page_cache' ) || ! CTPS_Settings::on( 'page_cache' ) ) { $reason = 'Page cache delegated to another provider'; } elseif ( '' === $reason ) { if ( $this->outer ) { $this->store_ok = true; // Stored by outer_buffer() with the final HTML. } else { CTPS_Page_Cache::store( $optimized ); } } if ( CTPS_Log::enabled() ) { CTPS_Log::add( 'optimize', '' === $reason ? 'Page optimized and cached' : 'Page optimized, not cached: ' . $reason, array_merge( $optimizer->stats(), array( 'ms' => (int) round( ( microtime( true ) - $started ) * 1000 ), 'bytes' => strlen( $html ) . '→' . strlen( $optimized ), ) ) ); } return $optimized; } /** * Prefetch of internal links on mouse hover (Speculation Rules). * WordPress 6.8+ already does it in "conservative" mode: here we make it more effective. */ private function prefetch_links() { if ( ! CTPS_Settings::on( 'prefetch_links' ) ) { return; } if ( function_exists( 'wp_get_speculation_rules_configuration' ) ) { add_filter( 'wp_speculation_rules_configuration', function ( $config ) { return is_array( $config ) ? array( 'mode' => 'prefetch', 'eagerness' => 'moderate' ) : $config; } ); return; } add_action( 'wp_footer', function () { if ( is_user_logged_in() ) { return; } $rules = array( 'prefetch' => array( array( 'source' => 'document', 'where' => array( 'and' => array( array( 'href_matches' => '/*' ), array( 'not' => array( 'href_matches' => array( '/wp-admin/*', '/wp-login.php*', '/*\\?*', '/carrello/*', '/cart/*', '/checkout/*', '/*.php' ) ) ), array( 'not' => array( 'selector_matches' => '.no-prefetch, [rel~=nofollow]' ) ), ), ), 'eagerness' => 'moderate', ), ), ); wp_print_inline_script_tag( wp_json_encode( $rules ), array( 'type' => 'speculationrules' ) ); } ); } public static function load_textdomain() { load_plugin_textdomain( 'celero-adaptive-speed', false, dirname( plugin_basename( WPADAPT_FILE ) ) . '/languages' ); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- loads the bundled translations when the plugin is installed outside WordPress.org } public static function activate() { // One cache folder, early config, drop-ins and .htaccess for the whole // network: multisite is not supported yet. Nothing is set up there and // the plugin only shows a notice (see the constructor). if ( is_multisite() ) { return; } // Activation must never fail just because an optional server feature // (Redis, writable .htaccess, cron, database DDL, mail, etc.) is // unavailable. WordPress should be able to finish installing the // plugin and the dashboard can report/repair the individual feature. $steps = array( function () { CTPS_Settings::flush_cache(); if ( false === get_option( CTPS_Settings::OPTION ) ) { // Safe Mode on first install: no JavaScript/CSS rewriting and no // feature that changes site behaviour until the user chooses it. add_option( CTPS_Settings::OPTION, CTPS_Settings::install_defaults() ); update_option( 'ctps_profile', 'safe', false ); } }, function () { wp_mkdir_p( CTPS_CACHE_DIR ); }, function () { delete_option( 'ctps_disabled_cleaned' ); }, function () { self::upgrade(); }, function () { CTPS_Safety::key(); }, // Recovery link, shown in the Safety section (never emailed unless asked). function () { CTPS_Preload::schedule(); }, function () { if ( ! get_option( 'ctps_wizard_done' ) ) { update_option( 'ctps_wizard_redirect', 1, false ); } }, ); foreach ( $steps as $step ) { try { call_user_func( $step ); } catch ( \Throwable $e ) { // Do not turn a recoverable hosting limitation into a failed // WordPress activation. Logging is best-effort as well. if ( class_exists( 'CTPS_Log' ) ) { try { CTPS_Log::error( $e, 'Activation' ); } catch ( \Throwable $ignored ) { // Intentionally ignored: activation must complete. } } } } } /** * Run on activation and when the plugin version changes. */ public static function upgrade() { // Older versions emailed the recovery link after activation: not any more. wp_clear_scheduled_hook( 'ctps_send_recovery_mail' ); $s = get_option( CTPS_Settings::OPTION ); if ( is_array( $s ) && isset( $s['delay_patterns'] ) && str_replace( "\r", '', $s['delay_patterns'] ) === CTPS_Settings::DELAY_OLD ) { $s['delay_patterns'] = CTPS_Settings::DELAY_DEFAULT; update_option( CTPS_Settings::OPTION, $s ); CTPS_Settings::flush_cache(); } if ( ! is_multisite() ) { // Drop-ins and .htaccess are shared by the whole network: single site only. if ( CTPS_Settings::on( 'htaccess_rules' ) && WPADAPT_Policy::can( 'htaccess_rules' ) ) { CTPS_Htaccess::write(); } CTPS_Object_Cache::maybe_update(); } if ( ! wp_next_scheduled( 'ctps_purge_expired' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', 'ctps_purge_expired' ); } if ( ! wp_next_scheduled( CTPS_PSI::HOOK ) ) { wp_schedule_event( time() + 10 * MINUTE_IN_SECONDS, 'daily', CTPS_PSI::HOOK ); } CTPS_Safety::key(); CTPS_RUM::install(); CTPS_Report::schedule(); if ( get_option( 'ctps_version' ) !== CTPS_VERSION ) { // Cached pages were generated by the previous version. CTPS_Page_Cache::purge_all(); } update_option( 'ctps_version', CTPS_VERSION ); } public static function deactivate() { if ( class_exists( 'WPADAPT_Early_Cache' ) ) { WPADAPT_Early_Cache::disable(); } CTPS_Object_Cache::on_deactivate(); CTPS_Htaccess::remove(); CTPS_Page_Cache::remove_all(); CTPS_Assets::remove_all(); wp_clear_scheduled_hook( 'ctps_purge_expired' ); CTPS_Webp_Replace::stop_bg(); wp_clear_scheduled_hook( CTPS_PSI::HOOK ); wp_clear_scheduled_hook( CTPS_Preload::HOOK ); wp_clear_scheduled_hook( CTPS_PSI::NEXT ); wp_clear_scheduled_hook( CTPS_Report::HOOK ); wp_clear_scheduled_hook( 'ctps_send_recovery_mail' ); wp_clear_scheduled_hook( 'wpadapt_experiment_cron' ); wp_clear_scheduled_hook( 'wpadapt_rum_guard_check' ); // Font downloads are scheduled with arguments: remove every instance. wp_unschedule_hook( CTPS_Fonts::HOOK ); delete_option( CTPS_Preload::QUEUE ); delete_option( 'ctps_version' ); } }