PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / class / class-mainwp-cache-warm-helper.php

class-mainwp-cache-warm-helper.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at class/class-mainwp-cache-warm-helper.php

474 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Warm Cache Helper.
4 *
5 * @package MainWP/Dashboard
6 */
7
8 namespace MainWP\Dashboard;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class MainWP_System
17 *
18 * @package MainWP\Dashboard
19 */
20 class MainWP_Cache_Warm_Helper { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
21
22 const WPC_VER = '1';
23
24 /**
25 * Private static variable to hold the single instance of the class.
26 *
27 * @var mixed Default null
28 */
29 private static $instance = null;
30
31 /**
32 * Private static variable to hold processed list of invalidated pages.
33 *
34 * @var mixed Default array
35 */
36 private static $invalidated_processed = array();
37
38 /**
39 * Method instance()
40 *
41 * Create a public static instance.
42 *
43 * @static
44 * @return MainWP_System
45 */
46 public static function instance() {
47 if ( null === static::$instance ) {
48 static::$instance = new self();
49 }
50 return static::$instance;
51 }
52
53 /**
54 * Constructor.
55 *
56 * Runs any time class is called.
57 */
58 public function __construct() { //phpcs:ignore -- NOSONAR - complex.
59 add_action( 'current_screen', array( __CLASS__, 'init_no_cache_header' ), 999 );
60 /**
61 * Action to Invalide warm cache pages.
62 *
63 * @since 5.5.
64 */
65 add_action( 'mainwp_invalidate_warm_cache_pages', array( __CLASS__, 'hook_warm_cache_invalidate_pages' ), 10, 2 );
66 }
67
68 /**
69 * Method init_no_cache_header()
70 */
71 public static function init_no_cache_header() {
72 if ( MainWP_System::is_mainwp_pages() ) {
73 $page = isset( $_GET['page'] ) ? wp_unslash( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
74 if ( ! headers_sent() && ! static::is_excluded_warm_cache_pages( $page ) ) {
75 $maxage = get_option( 'mainwp_warm_cache_pages_ttl', 10 );
76 if ( empty( $maxage ) ) {
77 return;
78 }
79 static::verify_current_page_no_cache_header();
80 }
81 }
82 }
83
84 /**
85 * Method hook_warm_cache_invalidate_pages().
86 *
87 * @param array $pages List pages to invalidate.
88 */
89 public static function hook_warm_cache_invalidate_pages( $pages = array() ) {
90 static::invalidate_manage_pages( $pages );
91 }
92
93 /**
94 * Method invalidate_manage_pages().
95 *
96 * @param array $pages List pages to invalidate.
97 */
98 public static function invalidate_manage_pages( $pages = array() ) {
99 if ( ! empty( $pages ) && is_array( $pages ) ) {
100 foreach ( $pages as $page ) {
101 static::invalidate_page_warm_cache( $page );
102 }
103 }
104 }
105
106 /**
107 * Method invalidate_pages_by_site_actions().
108 *
109 * @param array $action Site actions.
110 */
111 public static function invalidate_pages_by_site_actions( $action ) {
112 if ( 'stats' === $action ) {
113 static::invalidate_manage_pages( array( 'mainwp_tab', 'managesites' ) );
114 }
115 }
116
117 /**
118 * Method maybe_invalidate_page_by_params().
119 *
120 * @param array $page pages to invalidate.
121 * @param array $params Request params.
122 */
123 public static function maybe_invalidate_page_by_params( $page = '', $params = array() ) {
124 if ( ! empty( $page ) ) {
125 $page_key = static::get_warm_cache_page_key( $page );
126 $prefix_key = static::get_page_warm_cache_key_prefix( $page_key );
127 $prefix_params = static::generate_warm_cache_params_prefix( $params );
128 MainWP_Logger::instance()->log_events( 'warm-cache', 'Maybe Invalidate :: [page=' . $page . '] :: [params=' . print_r( $params, true ) . '] :: [prefix_key=' . $prefix_key . '] :: [prefix_params=' . $prefix_params . ']' ); //phpcs:ignore -- ok.
129 if ( $prefix_key !== $prefix_params ) {
130 // set new prefix.
131 static::set_page_warm_cache_key_prefix( $page_key, $prefix_params );
132 // invalidate the cache.
133 static::invalidate_page_warm_cache( $page );
134 } else {
135 MainWP_Logger::instance()->log_events( 'warm-cache', 'No Invalidate :: [page=' . $page . ']' );
136 }
137 }
138 }
139
140 /**
141 * Method verify_current_page_no_cache_header()
142 */
143 private static function verify_current_page_no_cache_header() { // phpcs:ignore -- NOSONAR -complex method.
144
145 $page = isset($_GET['page']) ? preg_replace('/[^a-z0-9_\-]/i', '', $_GET['page']) : ''; //phpcs:ignore -- ok.
146
147 if ( empty( $page ) ) {
148 return;
149 }
150
151 $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : ''; //phpcs:ignore -- WPCS: sanitization ok.
152 if ( ! in_array( $method, array( 'GET', 'HEAD' ) ) ) {
153 return;
154 }
155
156 // Your real "last time this page's data changed" (must be stable until content changes!).
157 $lastChangedTs = (int) static::get_current_page_warm_cache_last_changed();
158
159 if ( empty( $lastChangedTs ) ) {
160 return;
161 }
162
163 $etag = static::get_current_page_etag_header();
164
165 $lastModified = gmdate( 'D, d M Y H:i:s', $lastChangedTs ) . ' GMT';
166
167 // Clear conflicting headers first.
168 header_remove( 'Pragma' );
169 header_remove( 'Expires' );
170 header_remove( 'Cache-Control' );
171
172 // ====== Freshness policy ======
173 // Always revalidate before use (keeps a cached copy, but checks each navigation).
174 header( 'Cache-Control: private, no-cache' );
175 // If you prefer a TTL, use e.g.: header('Cache-Control: private, max-age=60, must-revalidate');.
176
177 header( "ETag: $etag" );
178 header( "Last-Modified: $lastModified" );
179 header( 'Vary: Accept-Encoding' ); // safe variant separation.
180
181 // ====== Conditional handling (304 if unchanged) ======.
182 $ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? ''; //phpcs:ignore -- ok.
183 $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? ''; //phpcs:ignore -- ok.
184
185 $normalize = static function ( string $t ): string {
186 $t = trim( $t );
187 if ( strncasecmp( $t, 'W/', 2 ) === 0 ) {
188 $t = substr( $t, 2 ); // ignore weak prefix.
189 }
190 return $t;
191 };
192
193 $etagMatch = false;
194 if ( '' !== $ifNoneMatch ) {
195 foreach ( explode( ',', $ifNoneMatch ) as $cand ) {
196 if ( $normalize( $cand ) === $etag ) {
197 $etagMatch = true;
198 break;
199 }
200 }
201 }
202
203 $lmMatch = ( '' !== $ifModifiedSince ) && ( strtotime( $ifModifiedSince ) >= $lastChangedTs );
204
205 // If either validator matches, nothing changed → 304.
206 if ( $etagMatch || $lmMatch ) {
207 http_response_code( 304 );
208 // Re-send validators with 304.
209 header( "ETag: $etag" );
210 header( "Last-Modified: $lastModified" );
211 exit;
212 }
213 }
214
215 /**
216 * Method get_page_warm_cache_last_change_key().
217 *
218 * @param string $page The page.
219 *
220 * @return string The key prefix.
221 */
222 public static function get_page_warm_cache_last_change_key( $page = '' ) {
223 $page_key = static::get_warm_cache_page_key( $page );
224 $prefix_key = static::get_page_warm_cache_key_prefix( $page_key );
225 if ( empty( $prefix_key ) ) {
226 return '';
227 }
228 return 'mainwp_warm_cache_page_last_changed_key_' . $prefix_key;
229 }
230
231 /**
232 * Method get_warm_cache_page_key().
233 *
234 * @param string $page The page.
235 *
236 * @return string The key.
237 */
238 public static function get_warm_cache_page_key( $page = '' ) {
239
240 if ( empty( $page ) ) {
241 $page = isset($_GET['page']) ? preg_replace('/[^a-z0-9_\-]/i', '', $_GET['page']) : ''; //phpcs:ignore -- ok.
242 if ( empty( $page ) ) {
243 return '';
244 }
245 }
246
247 // Per-user id.
248 if ( function_exists( 'get_current_user_id' ) ) {
249 $userId = (int) get_current_user_id();
250 } else {
251 $userId = 0;
252 }
253
254 return sha1( 'v:' . static::WPC_VER . "|u:$userId|p:$page" ); // NOSONAR - is sufficient for keys.
255 }
256
257
258 /**
259 * Method get_page_warm_cache_key_prefix().
260 *
261 * @param string $page_key The page key.
262 *
263 * @return string The key.
264 */
265 public static function get_page_warm_cache_key_prefix( $page_key = '' ) {
266
267 if ( empty( $page_key ) ) {
268 return '';
269 }
270
271 $prefix = get_transient( 'mainwp_warm_cache_last_changed_key_prefix_' . $page_key );
272
273 if ( empty( $prefix ) ) {
274 $prefix = static::set_page_warm_cache_key_prefix( $page_key ); // set default prefix.
275 }
276
277 return $prefix;
278 }
279
280 /**
281 * Method set_page_warm_cache_key_prefix().
282 *
283 * @param string $page_key The page key.
284 * @param string $prefix_params The params prefix.
285 *
286 * @return string The key.
287 */
288 public static function set_page_warm_cache_key_prefix( $page_key = '', $prefix_params = '' ) {
289
290 if ( empty( $page_key ) ) {
291 return '';
292 }
293
294 $maxage = (int) get_option( 'mainwp_warm_cache_pages_ttl', 10 );
295 $seconds = $maxage * 60;
296
297 if ( ! empty( $prefix_params ) ) {
298 $prefix_key = $prefix_params;
299 } else {
300 $prefix_key = $page_key;
301 }
302
303 set_transient( 'mainwp_warm_cache_last_changed_key_prefix_' . $page_key, $prefix_key, $seconds );
304
305 return $prefix_key;
306 }
307
308 /**
309 * Method generate_warm_cache_params_prefix().
310 *
311 * @param array $params The page params.
312 *
313 * @return string The key.
314 */
315 public static function generate_warm_cache_params_prefix( $params = array() ) {
316 $key_params = 'params:';
317 if ( ! empty( $params ) ) {
318 $normalized_key = MainWP_Cache_Helper::get_params_key( $params );
319 if ( ! empty( $normalized_key ) ) {
320 $key_params .= $normalized_key;
321 }
322 }
323 return sha1( $key_params ); // NOSONAR - is sufficient for keys.
324 }
325
326
327 /**
328 * Method get_current_page_warm_cache_last_changed().
329 *
330 * @return int Last change timestamp.
331 */
332 public static function get_current_page_warm_cache_last_changed() {
333
334 $lastchanged = (int) static::get_page_last_changed_cache();
335
336 $maxage = (int) get_option( 'mainwp_warm_cache_pages_ttl', 10 );
337 $seconds = $maxage * 60;
338
339 if ( empty( $lastchanged ) || (int) $lastchanged < time() - $seconds ) {
340 $lastchanged = time();
341 $last_changed_key = static::get_page_warm_cache_last_change_key();
342 if ( ! empty( $last_changed_key ) ) {
343 set_transient( $last_changed_key, $lastchanged, $seconds );
344 }
345 }
346
347 return $lastchanged;
348 }
349
350
351 /**
352 * Method get_current_page_etag_header().
353 *
354 * @return string Etag.
355 */
356 public static function get_current_page_etag_header() {
357 return static::get_page_etag_header();
358 }
359
360 /**
361 * Method get_page_etag_header().
362 *
363 * @param string $page Page to get Etag header.
364 *
365 * @return string Etag.
366 */
367 public static function get_page_etag_header( $page = '' ) {
368
369 if ( empty( $page ) ) {
370 $page = isset($_GET['page']) ? preg_replace('/[^a-z0-9_\-]/i', '', $_GET['page']) : ''; //phpcs:ignore -- ok.
371 if ( empty( $page ) ) {
372 return '';
373 }
374 }
375
376 $lastChangedTs = (int) static::get_page_last_changed_cache();
377 // Per-user id.
378 if ( function_exists( 'get_current_user_id' ) ) {
379 $userId = (int) get_current_user_id();
380 } else {
381 $userId = 0;
382 }
383 // ====== Build ETag from user + page + last-modified ======.
384 return '"mainwp_pages_' . sha1( "u:$userId|p:$page|ts:$lastChangedTs" ) . '"'; // NOSONAR - is sufficient for keys.
385 }
386
387
388 /**
389 * Method get_page_last_changed_cache().
390 *
391 * @param string $page Page to get last changed cache.
392 *
393 * @return mixed Page last change.
394 */
395 public static function get_page_last_changed_cache( $page = '' ) {
396 $last_changed_key = static::get_page_warm_cache_last_change_key( $page );
397 if ( empty( $last_changed_key ) ) {
398 return 0;
399 }
400 return get_transient( $last_changed_key );
401 }
402
403 /**
404 * Method invalidate_page_warm_cache().
405 *
406 * @param string $page Page to invalidate.
407 *
408 * @return mixed Invalidate last change timestamp.
409 */
410 private static function invalidate_page_warm_cache( $page ) {
411
412 if ( ! is_array( static::$invalidated_processed ) ) {
413 static::$invalidated_processed = array();
414 }
415
416 if ( isset( static::$invalidated_processed[ $page ] ) ) {
417 return false;
418 }
419
420 static::$invalidated_processed[ $page ] = 1;
421
422 $last_changed_key = static::get_page_warm_cache_last_change_key( $page );
423 MainWP_Logger::instance()->log_events( 'warm-cache', 'Invalidate :: [page=' . $page . '] :: [last_changed_key=' . $last_changed_key . ']' );
424 return static::invalidate_warm_cache_key( $last_changed_key );
425 }
426
427 /**
428 * Method invalidate_warm_cache_key().
429 *
430 * @param string $last_key Last key to invalidate.
431 *
432 * @return int Invalidate last change timestamp.
433 */
434 private static function invalidate_warm_cache_key( $last_key ) {
435 $lastchanged = time();
436 $maxage = get_option( 'mainwp_warm_cache_pages_ttl', 10 );
437 $seconds = $maxage * 60;
438 set_transient( $last_key, $lastchanged, $seconds );
439 return $lastchanged;
440 }
441
442 /**
443 * Method is_excluded_warm_cache_pages().
444 *
445 * @param mixed $page Check excluded pages or not.
446 *
447 * @return bool True|false, excluded or not.
448 */
449 public static function is_excluded_warm_cache_pages( $page = false ) {
450
451 if ( empty( $page ) ) {
452 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; //phpcs:ignore -- ok.
453 }
454
455 if ( empty( $page ) ) {
456 return true;
457 }
458
459 /**
460 * Excluded warm cache pages.
461 *
462 * @since 5.5.
463 */
464 $exclude_pages = apply_filters(
465 'mainwp_warm_cache_excluded_pages',
466 array(
467 'ActionLogs',
468 'mainwp-setup',
469 )
470 );
471 return ! is_array( $exclude_pages ) || in_array( '_excluded_all', $exclude_pages ) || ! in_array( $page, $exclude_pages ) ? true : false;
472 }
473 }
474