PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / trunk
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder vtrunk
2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 All 78 releases
ablocks / includes / performance / page-cache.php

page-cache.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder trunk, at includes/performance/page-cache.php

177 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocks\Performance;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use ABlocks\Classes\PageCache\Rules;
9 use ABlocks\Classes\PageCache\Store;
10
11 /**
12 * Performance Suite — full-page HTML cache (write path).
13 *
14 * Captures the rendered page and stores it on disk so later requests can be
15 * served without re-running WordPress, block parsing and theme rendering.
16 *
17 * This class owns the *write* side only. Serving happens far earlier in the
18 * request than any plugin class is loaded — see the serve hook registered from
19 * ablocks.php, the advanced-cache.php drop-in, and the nginx rule. All of them
20 * defer to {@see Rules} so every tier makes the same decision.
21 *
22 * Opt-in via `perf_page_cache` (default off). See docs/PAGE-CACHE-PLAN.md.
23 */
24 class PageCache {
25
26 /**
27 * Marker appended to cached copies, so `curl` shows whether a response came
28 * from disk and when it was generated. Never present in the live response.
29 */
30 const META_PREFIX = '<!-- ablocks-page-cache ';
31
32 public static function init() {
33 // Registered before the gates below: `purge` must stay available so a
34 // site owner can clear files left behind after switching the feature off.
35 if ( defined( 'WP_CLI' ) && \WP_CLI ) {
36 \ABlocks\Classes\PageCache\Cli::register();
37 }
38
39 // Maintenance is registered before the frontend gates below: pruning has
40 // to keep running in admin and cron contexts, which is where WP-Cron
41 // actually fires.
42 \ABlocks\Classes\PageCache\Scheduler::init();
43
44 // Also before the frontend gates: the toolbar has to appear both on the
45 // site and in wp-admin, and its action handler runs on admin-post.php.
46 \ABlocks\Classes\PageCache\AdminBar::init();
47
48 if ( is_admin() ) {
49 return;
50 }
51 if ( ! Rules::is_enabled() ) {
52 return;
53 }
54
55 $self = new self();
56
57 // Priority 0 on template_redirect: the earliest point at which the main
58 // query has run (so is_404()/is_search() are meaningful) while still
59 // being ahead of anything that renders. Starting first also makes this
60 // the outermost output buffer, and nested buffers flush inner-first —
61 // so the callback receives output after other plugins have transformed
62 // it, which is what we want to store.
63 add_action( 'template_redirect', [ $self, 'start_buffer' ], 0 );
64 }
65
66 /**
67 * Begin capturing output, unless the request is disqualified up front.
68 */
69 public function start_buffer() {
70 // Cheap superglobal-only gate. The expensive, WordPress-aware checks run
71 // at write time, when the response is actually known.
72 if ( Rules::should_bypass_request() ) {
73 $this->debug_header( 'bypass', Rules::last_reason() );
74 return;
75 }
76
77 ob_start( [ $this, 'maybe_cache' ] );
78 }
79
80 /**
81 * Output-buffer callback: decide, write, and always return the buffer intact.
82 *
83 * This runs during shutdown. It must never throw and must never alter the
84 * response — a caching layer that can break the page it is caching is worse
85 * than no caching layer.
86 *
87 * @param string $buffer Rendered output.
88 * @return string The same buffer, unmodified.
89 */
90 public function maybe_cache( $buffer ) {
91 if ( ! is_string( $buffer ) || '' === $buffer ) {
92 return $buffer;
93 }
94
95 try {
96 $reason = Rules::response_bypass_reason();
97 if ( null === $reason ) {
98 $reason = Rules::body_bypass_reason( $buffer );
99 }
100
101 if ( null !== $reason ) {
102 $this->debug_header( 'bypass', $reason );
103 return $buffer;
104 }
105
106 $file = Store::current_file_path();
107 if ( null === $file ) {
108 $this->debug_header( 'bypass', 'unrepresentable-path' );
109 return $buffer;
110 }
111
112 $gzip = (bool) \ABlocks\Helper::get_settings( 'perf_page_cache_gzip', true );
113 $written = Store::write( $file, $buffer . $this->meta_comment(), $gzip );
114
115 $this->debug_header( $written ? 'store' : 'bypass', $written ? null : 'write-failed' );
116 } catch ( \Throwable $e ) {
117 // Any failure here is a caching failure, not a page failure. Swallow
118 // it, note it when debugging, and serve the page as normal.
119 $this->debug_header( 'bypass', 'exception' );
120 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
121 error_log( 'aBlocks page cache: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
122 }
123 }//end try
124
125 return $buffer;
126 }
127
128 /**
129 * Trailing marker stored with the cached copy.
130 *
131 * Kept inside an HTML comment rather than a sidecar file: no extra inode per
132 * page, harmless when nginx serves the file directly, and it makes a cache
133 * hit self-identifying when someone curls the URL.
134 *
135 * @return string
136 */
137 private function meta_comment() {
138 $meta = [
139 'url' => home_url( Rules::request_path() ),
140 'created' => gmdate( 'c' ),
141 'version' => ABLOCKS_VERSION,
142 ];
143
144 $json = wp_json_encode( $meta );
145 if ( false === $json ) {
146 return '';
147 }
148
149 // Defensive: a comment body containing "--" or ">" would break out of the
150 // comment. None of the values above can, but the URL is request-derived.
151 $json = str_replace( [ '--', '>' ], [ '- -', '' ], $json );
152
153 return "\n" . self::META_PREFIX . $json . " -->\n";
154 }
155
156 /**
157 * Emit a diagnostic header while debugging.
158 *
159 * Only under WP_DEBUG: the reason string names cookie prefixes and query
160 * behaviour, which is useful to a developer and needless surface on a
161 * production response.
162 *
163 * @param string $state 'store' or 'bypass'.
164 * @param string|null $reason Bypass reason, when applicable.
165 */
166 private function debug_header( $state, $reason = null ) {
167 if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
168 return;
169 }
170 if ( headers_sent() ) {
171 return;
172 }
173 $value = $state . ( $reason ? '; ' . $reason : '' );
174 header( 'X-ABlocks-Cache: ' . $value );
175 }
176 }
177