optimize = GSSO_Optimizer::should_run(); if ( ! headers_sent() && GSSO_Page_Cache::enabled() ) { header( 'X-GSSO-Cache: ' . ( GSSO_Page_Cache::is_cacheable() ? 'MISS' : 'BYPASS' ) ); if ( GSSO_Settings::get( 'host_cache_nocache' ) ) { GSSO_Host_Cache::nocache_header(); } } if ( ! $this->optimize && ! GSSO_Page_Cache::is_cacheable() ) { return; } if ( GSSO_Request::buffer_level() ) { // Our own page buffer is open; process it at shutdown, before WordPress flushes all buffers (priority 1). add_action( 'shutdown', array( $this, 'finish_output' ), 0 ); return; } // Fallback: WordPress core's template enhancement buffer. if ( $this->optimize ) { // Late priority: optimize the markup after other enhancement filters have run. add_filter( 'wp_template_enhancement_output_buffer', array( $this, 'optimize_html' ), 100 ); } if ( GSSO_Page_Cache::is_cacheable() ) { // Fires with the final HTML just before core flushes its buffer. add_action( 'wp_finalized_template_enhancement_output_buffer', array( $this, 'store_html' ), 100 ); } } /** * Close the page buffer: flush any buffers other plugins opened on top of * it into it, take the finished page, optimize and cache it, and send it. */ public function finish_output() { $level = GSSO_Request::buffer_level(); if ( ! $level ) { return; } $status = ob_get_status( true ); if ( ob_get_level() < $level || ! isset( $status[ $level - 1 ]['name'] ) || 'GSSO_Request::buffer_handler' !== $status[ $level - 1 ]['name'] ) { GSSO_Request::buffer_closed(); GSSO_Logger::notice_once( 'buffer-lost', 'Another plugin closed the page output buffer early, so a page could not be optimized or cached.' ); return; } while ( ob_get_level() > $level ) { $top = ob_get_status(); if ( empty( $top['flags'] ) || ! ( $top['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE ) || ! ob_end_flush() ) { return; // A buffer above ours can't be closed; let WordPress flush everything normally. } } $html = (string) ob_get_contents(); ob_end_clean(); GSSO_Request::buffer_closed(); if ( $this->is_html_response() ) { if ( $this->optimize ) { $html = $this->optimize_html( $html ); } if ( GSSO_Page_Cache::is_cacheable() ) { $this->store_html( $html ); } } echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The site's own page, already rendered by WordPress. } private function is_html_response() { foreach ( headers_list() as $header ) { if ( 0 === stripos( $header, 'content-type:' ) ) { return false !== stripos( $header, 'text/html' ) || false !== stripos( $header, 'application/xhtml+xml' ); } } return true; } /** * @param string $html Full HTML document from the core output buffer. * @return string */ public function optimize_html( $html ) { try { $optimizer = new GSSO_Optimizer(); return $optimizer->process( (string) $html ); } catch ( \Throwable $e ) { GSSO_Logger::error( 'Optimization failed, page served unmodified: ' . $e->getMessage() ); return (string) $html; } } /** @param string $html Final HTML sent to the visitor. */ public function store_html( $html ) { try { GSSO_Page_Cache::maybe_store( (string) $html ); } catch ( \Throwable $e ) { GSSO_Logger::error( 'Could not store page in cache: ' . $e->getMessage() ); } } /* ---------- Lifecycle ---------- */ public static function activate() { GSSO_Filesystem::mkdir( GSSO_CACHE_DIR ); if ( false === get_option( GSSO_Settings::OPTION ) ) { add_option( GSSO_Settings::OPTION, GSSO_Settings::defaults() ); } update_option( 'gsso_version', GSSO_VERSION ); self::migrate_legacy(); if ( ! wp_next_scheduled( 'gsso_daily' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'gsso_daily' ); } // Server files are written on the first admin page load after activation, // so a problem can be reported in the UI instead of failing silently. set_transient( 'gsso_apply_pending', 1, HOUR_IN_SECONDS ); } /** * Clean up what older versions left behind. Runs on upgrade and on activation * (a renamed plugin folder is "activated", not "upgraded"). */ public static function migrate_legacy() { GSSO_Settings::migrate(); // 1.0-1.2 kept wp-config.php/.htaccess backups and a PHP config here. Remove them. GSSO_Filesystem::rrmdir( GSSO_LEGACY_CONFIG_DIR ); delete_transient( 'gsso_packages_feed' ); wp_clear_scheduled_hook( 'gsso_refresh_packages' ); } public static function deactivate() { // Stop the purge/preload hooks that fire on 'deactivated_plugin' from // recreating anything after cleanup. if ( ! defined( 'GSSO_DEACTIVATING' ) ) { define( 'GSSO_DEACTIVATING', true ); } GSSO_Dropin::uninstall(); GSSO_Htaccess::remove(); GSSO_Config_File::delete(); GSSO_Filesystem::rrmdir( GSSO_CACHE_DIR . '/pages', true ); GSSO_Preload::stop(); wp_clear_scheduled_hook( 'gsso_daily' ); wp_clear_scheduled_hook( GSSO_Database::HOOK ); wp_clear_scheduled_hook( 'gsso_refresh_packages' ); // Removed in 1.3.0. } public static function upgrade() { update_option( 'gsso_version', GSSO_VERSION ); self::migrate_legacy(); self::apply_settings(); } /** * Push the current settings out to every place they live: the drop-in * config, advanced-cache.php / WP_CACHE, .htaccess, and cron. * * @return string[] Problems worth showing to the admin. */ public static function apply_settings() { GSSO_Settings::flush_cache(); $messages = array(); if ( ! GSSO_Config_File::write() ) { $messages[] = 'The cache configuration could not be written. Make sure wp-content/cache is writable.'; } if ( GSSO_Page_Cache::enabled() ) { $result = GSSO_Dropin::install(); if ( ! $result['ok'] && $result['message'] ) { $messages[] = $result['message']; } } else { GSSO_Dropin::uninstall(); GSSO_Filesystem::rrmdir( GSSO_CACHE_DIR . '/pages', true ); } $result = GSSO_Htaccess::apply(); if ( ! $result['ok'] && $result['message'] ) { $messages[] = $result['message']; } GSSO_Database::reschedule(); if ( ! wp_next_scheduled( 'gsso_daily' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'gsso_daily' ); } return $messages; } /** Prune minified files that no cached page can still reference. */ public static function daily_maintenance() { $lifespan = (int) GSSO_Settings::get( 'cache_lifespan' ) * HOUR_IN_SECONDS; if ( $lifespan <= 0 ) { return; // Pages never expire, so their assets must not either. } $max_age = max( 30 * DAY_IN_SECONDS, $lifespan * 3 ); foreach ( array( 'css', 'js' ) as $type ) { foreach ( (array) glob( GSSO_CACHE_DIR . '/assets/' . $type . '/*.' . $type ) as $file ) { if ( $file && ( time() - (int) @filemtime( $file ) ) > $max_age ) { wp_delete_file( $file ); } } } } }