debug_header( 'bypass', Rules::last_reason() ); return; } ob_start( [ $this, 'maybe_cache' ] ); } /** * Output-buffer callback: decide, write, and always return the buffer intact. * * This runs during shutdown. It must never throw and must never alter the * response — a caching layer that can break the page it is caching is worse * than no caching layer. * * @param string $buffer Rendered output. * @return string The same buffer, unmodified. */ public function maybe_cache( $buffer ) { if ( ! is_string( $buffer ) || '' === $buffer ) { return $buffer; } try { $reason = Rules::response_bypass_reason(); if ( null === $reason ) { $reason = Rules::body_bypass_reason( $buffer ); } if ( null !== $reason ) { $this->debug_header( 'bypass', $reason ); return $buffer; } $file = Store::current_file_path(); if ( null === $file ) { $this->debug_header( 'bypass', 'unrepresentable-path' ); return $buffer; } $gzip = (bool) \ABlocks\Helper::get_settings( 'perf_page_cache_gzip', true ); $written = Store::write( $file, $buffer . $this->meta_comment(), $gzip ); $this->debug_header( $written ? 'store' : 'bypass', $written ? null : 'write-failed' ); } catch ( \Throwable $e ) { // Any failure here is a caching failure, not a page failure. Swallow // it, note it when debugging, and serve the page as normal. $this->debug_header( 'bypass', 'exception' ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'aBlocks page cache: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log } }//end try return $buffer; } /** * Trailing marker stored with the cached copy. * * Kept inside an HTML comment rather than a sidecar file: no extra inode per * page, harmless when nginx serves the file directly, and it makes a cache * hit self-identifying when someone curls the URL. * * @return string */ private function meta_comment() { $meta = [ 'url' => home_url( Rules::request_path() ), 'created' => gmdate( 'c' ), 'version' => ABLOCKS_VERSION, ]; $json = wp_json_encode( $meta ); if ( false === $json ) { return ''; } // Defensive: a comment body containing "--" or ">" would break out of the // comment. None of the values above can, but the URL is request-derived. $json = str_replace( [ '--', '>' ], [ '- -', '' ], $json ); return "\n" . self::META_PREFIX . $json . " -->\n"; } /** * Emit a diagnostic header while debugging. * * Only under WP_DEBUG: the reason string names cookie prefixes and query * behaviour, which is useful to a developer and needless surface on a * production response. * * @param string $state 'store' or 'bypass'. * @param string|null $reason Bypass reason, when applicable. */ private function debug_header( $state, $reason = null ) { if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; } if ( headers_sent() ) { return; } $value = $state . ( $reason ? '; ' . $reason : '' ); header( 'X-ABlocks-Cache: ' . $value ); } }