PluginProbe ʕ •ᴥ•ʔ
LiteSpeed Cache / 7.6.1
LiteSpeed Cache v7.6.1
trunk 1.0.15 1.9.1.1 2.9.9.2 3.6.4 4.6 5.7.0.1 6.5.4 7.0.0.1 7.0.1 7.1 7.2 7.3 7.3.0.1 7.4 7.5 7.5.0.1 7.6 7.6.1 7.6.2 7.7 7.8 7.8.0.1 7.8.1
litespeed-cache / src / control.cls.php
litespeed-cache / src Last commit date
cdn 7 months ago data_structure 7 months ago activation.cls.php 7 months ago admin-display.cls.php 7 months ago admin-settings.cls.php 7 months ago admin.cls.php 7 months ago api.cls.php 7 months ago avatar.cls.php 7 months ago base.cls.php 7 months ago cdn.cls.php 7 months ago cloud.cls.php 7 months ago conf.cls.php 7 months ago control.cls.php 7 months ago core.cls.php 7 months ago crawler-map.cls.php 7 months ago crawler.cls.php 7 months ago css.cls.php 7 months ago data.cls.php 7 months ago data.upgrade.func.php 7 months ago db-optm.cls.php 7 months ago debug2.cls.php 7 months ago doc.cls.php 7 months ago error.cls.php 7 months ago esi.cls.php 7 months ago file.cls.php 7 months ago gui.cls.php 7 months ago health.cls.php 7 months ago htaccess.cls.php 7 months ago img-optm.cls.php 7 months ago import.cls.php 7 months ago import.preset.cls.php 7 months ago lang.cls.php 7 months ago localization.cls.php 7 months ago media.cls.php 7 months ago metabox.cls.php 7 months ago object-cache-wp.cls.php 7 months ago object-cache.cls.php 7 months ago object.lib.php 7 months ago optimize.cls.php 7 months ago optimizer.cls.php 7 months ago placeholder.cls.php 7 months ago purge.cls.php 7 months ago report.cls.php 7 months ago rest.cls.php 7 months ago root.cls.php 7 months ago router.cls.php 7 months ago str.cls.php 7 months ago tag.cls.php 7 months ago task.cls.php 7 months ago tool.cls.php 7 months ago ucss.cls.php 7 months ago utility.cls.php 7 months ago vary.cls.php 7 months ago vpi.cls.php 7 months ago
control.cls.php
957 lines
1 <?php
2 /**
3 * The plugin cache-control class for X-LiteSpeed-Cache-Control.
4 *
5 * Provides helpers for determining cacheability, emitting cache-control headers,
6 * and honoring various LiteSpeed Cache configuration options.
7 *
8 * @package LiteSpeed
9 * @since 1.1.3
10 */
11
12 namespace LiteSpeed;
13
14 defined( 'WPINC' ) || exit();
15
16 /**
17 * Class Control
18 *
19 * Handles cache-control flags, TTL calculation, redirection checks,
20 * role-based exclusions, and final header output.
21 */
22 class Control extends Root {
23
24 const LOG_TAG = '💵';
25
26 const BM_CACHEABLE = 1;
27 const BM_PRIVATE = 2;
28 const BM_SHARED = 4;
29 const BM_NO_VARY = 8;
30 const BM_FORCED_CACHEABLE = 32;
31 const BM_PUBLIC_FORCED = 64;
32 const BM_STALE = 128;
33 const BM_NOTCACHEABLE = 256;
34
35 const X_HEADER = 'X-LiteSpeed-Cache-Control';
36
37 /**
38 * Bitmask control flags for current request.
39 *
40 * @var int
41 */
42 protected static $_control = 0;
43
44 /**
45 * Custom TTL for current request (seconds).
46 *
47 * @var int
48 */
49 protected static $_custom_ttl = 0;
50
51 /**
52 * Mapping of HTTP status codes to custom TTLs.
53 *
54 * @var array<string,int|string>
55 */
56 private $_response_header_ttls = [];
57
58 /**
59 * Init cache control.
60 *
61 * @since 1.6.2
62 * @return void
63 */
64 public function init() {
65 /**
66 * Add vary filter for Role Excludes.
67 *
68 * @since 1.6.2
69 */
70 add_filter( 'litespeed_vary', [ $this, 'vary_add_role_exclude' ] );
71
72 // 301 redirect hook.
73 add_filter( 'wp_redirect', [ $this, 'check_redirect' ], 10, 2 );
74
75 // Load response header conf.
76 $this->_response_header_ttls = $this->conf( Base::O_CACHE_TTL_STATUS );
77 foreach ( $this->_response_header_ttls as $k => $v ) {
78 $v = explode( ' ', $v );
79 if ( empty( $v[0] ) || empty( $v[1] ) ) {
80 continue;
81 }
82 $this->_response_header_ttls[ $v[0] ] = $v[1];
83 }
84
85 if ( $this->conf( Base::O_PURGE_STALE ) ) {
86 $this->set_stale();
87 }
88 }
89
90 /**
91 * Exclude role from optimization filter.
92 *
93 * @since 1.6.2
94 * @access public
95 *
96 * @param array<string,mixed> $vary Existing vary map.
97 * @return array<string,mixed>
98 */
99 public function vary_add_role_exclude( $vary ) {
100 if ( $this->in_cache_exc_roles() ) {
101 $vary['role_exclude_cache'] = 1;
102 }
103
104 return $vary;
105 }
106
107 /**
108 * Check if one user role is in exclude cache group settings.
109 *
110 * @since 1.6.2
111 * @since 3.0 Moved here from conf.cls
112 * @access public
113 *
114 * @param string|null $role The user role.
115 * @return string|false Comma-separated roles if set, otherwise false.
116 */
117 public function in_cache_exc_roles( $role = null ) {
118 // Get user role.
119 if ( null === $role ) {
120 $role = Router::get_role();
121 }
122
123 if ( ! $role ) {
124 return false;
125 }
126
127 $roles = explode( ',', $role );
128 $found = array_intersect( $roles, $this->conf( Base::O_CACHE_EXC_ROLES ) );
129
130 return $found ? implode( ',', $found ) : false;
131 }
132
133 /**
134 * 1. Initialize cacheable status for `wp` hook
135 * 2. Hook error page tags for cacheable pages
136 *
137 * @since 1.1.3
138 * @access public
139 * @return void
140 */
141 public function init_cacheable() {
142 // Hook `wp` to mark default cacheable status.
143 // NOTE: Any process that does NOT run into `wp` hook will not get cacheable by default.
144 add_action( 'wp', [ $this, 'set_cacheable' ], 5 );
145
146 // Hook WP REST to be cacheable.
147 if ( $this->conf( Base::O_CACHE_REST ) ) {
148 add_action( 'rest_api_init', [ $this, 'set_cacheable' ], 5 );
149 }
150
151 // AJAX cache.
152 $ajax_cache = $this->conf( Base::O_CACHE_AJAX_TTL );
153 foreach ( $ajax_cache as $v ) {
154 $v = explode( ' ', $v );
155 if ( empty( $v[0] ) || empty( $v[1] ) ) {
156 continue;
157 }
158 add_action(
159 'wp_ajax_nopriv_' . $v[0],
160 function () use ( $v ) {
161 self::set_custom_ttl( $v[1] );
162 self::force_cacheable( 'ajax Cache setting for action ' . $v[0] );
163 },
164 4
165 );
166 }
167
168 // Check error page.
169 add_filter( 'status_header', [ $this, 'check_error_codes' ], 10, 2 );
170 }
171
172 /**
173 * Check if the page returns any error code.
174 *
175 * @since 1.0.13.1
176 * @access public
177 *
178 * @param string $status_header Status header.
179 * @param int $code HTTP status code.
180 * @return string Original status header.
181 */
182 public function check_error_codes( $status_header, $code ) {
183 if ( array_key_exists( $code, $this->_response_header_ttls ) ) {
184 if ( self::is_cacheable() && ! $this->_response_header_ttls[ $code ] ) {
185 self::set_nocache( '[Ctrl] TTL is set to no cache [status_header] ' . $code );
186 }
187
188 // Set TTL.
189 self::set_custom_ttl( $this->_response_header_ttls[ $code ] );
190 } elseif ( self::is_cacheable() ) {
191 $first = substr( $code, 0, 1 );
192 if ( '4' === $first || '5' === $first ) {
193 self::set_nocache( '[Ctrl] 4xx/5xx default to no cache [status_header] ' . $code );
194 }
195 }
196
197 // Set cache tag.
198 if ( in_array( $code, Tag::$error_code_tags, true ) ) {
199 Tag::add( Tag::TYPE_HTTP . $code );
200 }
201
202 // Give the default status_header back.
203 return $status_header;
204 }
205
206 /**
207 * Set no vary setting.
208 *
209 * @access public
210 * @since 1.1.3
211 * @return void
212 */
213 public static function set_no_vary() {
214 if ( self::is_no_vary() ) {
215 return;
216 }
217 self::$_control |= self::BM_NO_VARY;
218 self::debug( 'X Cache_control -> no-vary', 3 );
219 }
220
221 /**
222 * Get no vary setting.
223 *
224 * @access public
225 * @since 1.1.3
226 * @return bool
227 */
228 public static function is_no_vary() {
229 return self::$_control & self::BM_NO_VARY;
230 }
231
232 /**
233 * Set stale.
234 *
235 * @access public
236 * @since 1.1.3
237 * @return void
238 */
239 public function set_stale() {
240 if ( self::is_stale() ) {
241 return;
242 }
243 self::$_control |= self::BM_STALE;
244 self::debug( 'X Cache_control -> stale' );
245 }
246
247 /**
248 * Get stale.
249 *
250 * @access public
251 * @since 1.1.3
252 * @return bool
253 */
254 public static function is_stale() {
255 return self::$_control & self::BM_STALE;
256 }
257
258 /**
259 * Set cache control to shared private.
260 *
261 * @access public
262 * @since 1.1.3
263 *
264 * @param string|false $reason The reason to mark shared, or false.
265 * @return void
266 */
267 public static function set_shared( $reason = false ) {
268 if ( self::is_shared() ) {
269 return;
270 }
271 self::$_control |= self::BM_SHARED;
272 self::set_private();
273
274 if ( ! is_string( $reason ) ) {
275 $reason = false;
276 }
277
278 if ( $reason ) {
279 $reason = "( $reason )";
280 }
281 self::debug( 'X Cache_control -> shared ' . $reason );
282 }
283
284 /**
285 * Check if is shared private.
286 *
287 * @access public
288 * @since 1.1.3
289 * @return bool
290 */
291 public static function is_shared() {
292 return (bool) ( self::$_control & self::BM_SHARED ) && self::is_private();
293 }
294
295 /**
296 * Set cache control to forced public.
297 *
298 * @access public
299 * @since 1.7.1
300 *
301 * @param string|false $reason Reason text or false.
302 * @return void
303 */
304 public static function set_public_forced( $reason = false ) {
305 if ( self::is_public_forced() ) {
306 return;
307 }
308 self::$_control |= self::BM_PUBLIC_FORCED;
309
310 if ( ! is_string( $reason ) ) {
311 $reason = false;
312 }
313
314 if ( $reason ) {
315 $reason = "( $reason )";
316 }
317 self::debug( 'X Cache_control -> public forced ' . $reason );
318 }
319
320 /**
321 * Check if is public forced.
322 *
323 * @access public
324 * @since 1.7.1
325 * @return bool
326 */
327 public static function is_public_forced() {
328 return self::$_control & self::BM_PUBLIC_FORCED;
329 }
330
331 /**
332 * Set cache control to private.
333 *
334 * @access public
335 * @since 1.1.3
336 *
337 * @param string|false $reason The reason to set private.
338 * @return void
339 */
340 public static function set_private( $reason = false ) {
341 if ( self::is_private() ) {
342 return;
343 }
344 self::$_control |= self::BM_PRIVATE;
345
346 if ( ! is_string( $reason ) ) {
347 $reason = false;
348 }
349
350 if ( $reason ) {
351 $reason = "( $reason )";
352 }
353 self::debug( 'X Cache_control -> private ' . $reason );
354 }
355
356 /**
357 * Check if is private.
358 *
359 * @access public
360 * @since 1.1.3
361 * @return bool
362 */
363 public static function is_private() {
364 // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) {
365 // return false;
366 // }
367
368 return (bool) ( self::$_control & self::BM_PRIVATE ) && ! self::is_public_forced();
369 }
370
371 /**
372 * Initialize cacheable status in `wp` hook, if not call this, by default it will be non-cacheable.
373 *
374 * @access public
375 * @since 1.1.3
376 *
377 * @param string|false $reason Reason text or false.
378 * @return void
379 */
380 public function set_cacheable( $reason = false ) {
381 self::$_control |= self::BM_CACHEABLE;
382
383 if ( ! is_string( $reason ) ) {
384 $reason = false;
385 }
386
387 if ( $reason ) {
388 $reason = ' [reason] ' . $reason;
389 }
390 self::debug( 'Cache_control init on' . $reason );
391 }
392
393 /**
394 * This will disable non-cacheable BM.
395 *
396 * @access public
397 * @since 2.2
398 *
399 * @param string|false $reason Reason text or false.
400 * @return void
401 */
402 public static function force_cacheable( $reason = false ) {
403 self::$_control |= self::BM_FORCED_CACHEABLE;
404
405 if ( ! is_string( $reason ) ) {
406 $reason = false;
407 }
408
409 if ( $reason ) {
410 $reason = ' [reason] ' . $reason;
411 }
412 self::debug( 'Forced cacheable' . $reason );
413 }
414
415 /**
416 * Switch to nocacheable status.
417 *
418 * @access public
419 * @since 1.1.3
420 *
421 * @param string|false $reason The reason to no cache.
422 * @return void
423 */
424 public static function set_nocache( $reason = false ) {
425 self::$_control |= self::BM_NOTCACHEABLE;
426
427 if ( ! is_string( $reason ) ) {
428 $reason = false;
429 }
430
431 if ( $reason ) {
432 $reason = "( $reason )";
433 }
434 self::debug( 'X Cache_control -> no Cache ' . $reason, 5 );
435 }
436
437 /**
438 * Check current notcacheable bit set.
439 *
440 * @access public
441 * @since 1.1.3
442 * @return bool True if notcacheable bit is set, otherwise false.
443 */
444 public static function isset_notcacheable() {
445 return self::$_control & self::BM_NOTCACHEABLE;
446 }
447
448 /**
449 * Check current force cacheable bit set.
450 *
451 * @access public
452 * @since 2.2
453 * @return bool
454 */
455 public static function is_forced_cacheable() {
456 return self::$_control & self::BM_FORCED_CACHEABLE;
457 }
458
459 /**
460 * Check current cacheable status.
461 *
462 * @access public
463 * @since 1.1.3
464 * @return bool True if is still cacheable, otherwise false.
465 */
466 public static function is_cacheable() {
467 if ( defined( 'LSCACHE_NO_CACHE' ) && LSCACHE_NO_CACHE ) {
468 self::debug( 'LSCACHE_NO_CACHE constant defined' );
469 return false;
470 }
471
472 // Guest mode always cacheable
473 // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) {
474 // return true;
475 // }
476
477 // If it's forced public cacheable.
478 if ( self::is_public_forced() ) {
479 return true;
480 }
481
482 // If it's forced cacheable.
483 if ( self::is_forced_cacheable() ) {
484 return true;
485 }
486
487 return ! self::isset_notcacheable() && ( self::$_control & self::BM_CACHEABLE );
488 }
489
490 /**
491 * Set a custom TTL to use with the request if needed.
492 *
493 * @access public
494 * @since 1.1.3
495 *
496 * @param int|string $ttl An integer or numeric string to use as the TTL.
497 * @param string|false $reason Optional reason text.
498 * @return void
499 */
500 public static function set_custom_ttl( $ttl, $reason = false ) {
501 if ( is_numeric( $ttl ) ) {
502 self::$_custom_ttl = (int) $ttl;
503 self::debug( 'X Cache_control TTL -> ' . $ttl . ( $reason ? ' [reason] ' . $ttl : '' ) );
504 }
505 }
506
507 /**
508 * Generate final TTL.
509 *
510 * @access public
511 * @since 1.1.3
512 * @return int
513 */
514 public function get_ttl() {
515 if ( 0 !== self::$_custom_ttl ) {
516 return (int) self::$_custom_ttl;
517 }
518
519 // Check if is in timed url list or not.
520 $timed_urls = Utility::wildcard2regex( $this->conf( Base::O_PURGE_TIMED_URLS ) );
521 $timed_urls_time = $this->conf( Base::O_PURGE_TIMED_URLS_TIME );
522 if ( $timed_urls && $timed_urls_time ) {
523 $current_url = Tag::build_uri_tag( true );
524 // Use time limit ttl.
525 $scheduled_time = strtotime( $timed_urls_time );
526 $ttl = $scheduled_time - current_time('timestamp'); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp
527 if ( $ttl < 0 ) {
528 $ttl += 86400; // add one day
529 }
530 foreach ( $timed_urls as $v ) {
531 if ( false !== strpos( $v, '*' ) ) {
532 if ( preg_match( '#' . $v . '#iU', $current_url ) ) {
533 self::debug( 'X Cache_control TTL is limited to ' . $ttl . ' due to scheduled purge regex ' . $v );
534 return $ttl;
535 }
536 } elseif ( $v === $current_url ) {
537 self::debug( 'X Cache_control TTL is limited to ' . $ttl . ' due to scheduled purge rule ' . $v );
538 return $ttl;
539 }
540 }
541 }
542
543 // Private cache uses private ttl setting.
544 if ( self::is_private() ) {
545 return (int) $this->conf( Base::O_CACHE_TTL_PRIV );
546 }
547
548 if ( is_front_page() ) {
549 return (int) $this->conf( Base::O_CACHE_TTL_FRONTPAGE );
550 }
551
552 $feed_ttl = (int) $this->conf( Base::O_CACHE_TTL_FEED );
553 if ( is_feed() && $feed_ttl > 0 ) {
554 return $feed_ttl;
555 }
556
557 if ( $this->cls( 'REST' )->is_rest() || $this->cls( 'REST' )->is_internal_rest() ) {
558 return (int) $this->conf( Base::O_CACHE_TTL_REST );
559 }
560
561 return (int) $this->conf( Base::O_CACHE_TTL_PUB );
562 }
563
564 /**
565 * Check if need to set no cache status for redirection or not.
566 *
567 * @access public
568 * @since 1.1.3
569 *
570 * @param string $location Redirect location.
571 * @param int $status HTTP status.
572 * @return string Redirect location.
573 */
574 public function check_redirect( $location, $status ) {
575 $script_uri = '';
576 if ( !empty( $_SERVER['SCRIPT_URI'] ) ) {
577 $script_uri = sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_URI'] ) );
578 } elseif ( !empty( $_SERVER['REQUEST_URI'] ) ) {
579 $home = trailingslashit( home_url() );
580 $script_uri = $home . ltrim( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), '/' );
581 }
582
583 if ( '' !== $script_uri ) {
584 self::debug( '301 from ' . $script_uri );
585 self::debug( '301 to ' . $location );
586
587 $to_check = [ PHP_URL_SCHEME, PHP_URL_HOST, PHP_URL_PATH, PHP_URL_QUERY ];
588
589 $is_same_redirect = true;
590
591 $query_string = ! empty( $_SERVER['QUERY_STRING'] ) ? sanitize_text_field( wp_unslash( $_SERVER['QUERY_STRING'] ) ) : '';
592 foreach ( $to_check as $v ) {
593 $url_parsed = PHP_URL_QUERY === $v ? $query_string : wp_parse_url( $script_uri, $v );
594
595 $target = wp_parse_url( $location, $v );
596
597 self::debug( 'Compare [from] ' . $url_parsed . ' [to] ' . $target );
598
599 if ( PHP_URL_QUERY === $v ) {
600 $url_parsed = $url_parsed ? urldecode( $url_parsed ) : '';
601 $target = $target ? urldecode( $target ) : '';
602 if ( '&' === substr( $url_parsed, -1 ) ) {
603 $url_parsed = substr( $url_parsed, 0, -1 );
604 }
605 }
606
607 if ( $url_parsed !== $target ) {
608 $is_same_redirect = false;
609 self::debug( '301 different redirection' );
610 break;
611 }
612 }
613
614 if ( $is_same_redirect ) {
615 self::set_nocache( '301 to same url' );
616 }
617 }
618
619 return $location;
620 }
621
622 /**
623 * Sets up the Cache Control header.
624 *
625 * @since 1.1.3
626 * @access public
627 * @return string empty string if empty, otherwise the cache control header.
628 */
629 public function output() {
630 $esi_hdr = '';
631 if ( ESI::has_esi() ) {
632 $esi_hdr = ',esi=on';
633 }
634
635 $hdr = self::X_HEADER . ': ';
636
637 // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
638 if ( defined( 'DONOTCACHEPAGE' ) && apply_filters( 'litespeed_const_DONOTCACHEPAGE', DONOTCACHEPAGE ) ) {
639 self::debug( '❌ forced no cache [reason] DONOTCACHEPAGE const' );
640 $hdr .= 'no-cache' . $esi_hdr;
641 return $hdr;
642 }
643
644 // Guest mode directly return cacheable result
645 // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) {
646 // If is POST, no cache
647 // if ( defined( 'LSCACHE_NO_CACHE' ) && LSCACHE_NO_CACHE ) {
648 // self::debug( "[Ctrl] ❌ forced no cache [reason] LSCACHE_NO_CACHE const" );
649 // $hdr .= 'no-cache';
650 // }
651 // else if( $_SERVER[ 'REQUEST_METHOD' ] !== 'GET' ) {
652 // self::debug( "[Ctrl] ❌ forced no cache [reason] req not GET" );
653 // $hdr .= 'no-cache';
654 // }
655 // else {
656 // $hdr .= 'public';
657 // $hdr .= ',max-age=' . $this->get_ttl();
658 // }
659
660 // $hdr .= $esi_hdr;
661
662 // return $hdr;
663 // }
664
665 // Fix cli `uninstall --deactivate` fatal err
666
667 if (!self::is_cacheable()) {
668 $hdr .= 'no-cache' . $esi_hdr;
669 return $hdr;
670 }
671
672 if ( self::is_shared() ) {
673 $hdr .= 'shared,private';
674 } elseif ( self::is_private() ) {
675 $hdr .= 'private';
676 } else {
677 $hdr .= 'public';
678 }
679
680 if ( self::is_no_vary() ) {
681 $hdr .= ',no-vary';
682 }
683
684 $hdr .= ',max-age=' . $this->get_ttl() . $esi_hdr;
685 return $hdr;
686 }
687
688 /**
689 * Generate all `control` tags before output.
690 *
691 * @access public
692 * @since 1.1.3
693 * @return void
694 */
695 public function finalize() {
696 // if ( defined( 'LITESPEED_GUEST' ) && LITESPEED_GUEST ) {
697 // return;
698 // }
699
700 if ( is_preview() ) {
701 self::set_nocache( 'preview page' );
702 return;
703 }
704
705 // Check if has metabox non-cacheable setting or not.
706 if ( file_exists( LSCWP_DIR . 'src/metabox.cls.php' ) && $this->cls( 'Metabox' )->setting( 'litespeed_no_cache' ) ) {
707 self::set_nocache( 'per post metabox setting' );
708 return;
709 }
710
711 // Check if URI is forced public cache.
712 $excludes = $this->conf( Base::O_CACHE_FORCE_PUB_URI );
713 $req_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
714 $hit = Utility::str_hit_array( $req_uri, $excludes, true );
715 if ( $hit ) {
716 list( $result, $this_ttl ) = $hit;
717 self::set_public_forced( 'Setting: ' . $result );
718 self::debug( 'Forced public cacheable due to setting: ' . $result );
719 if ( $this_ttl ) {
720 self::set_custom_ttl( $this_ttl );
721 }
722 }
723
724 if ( self::is_public_forced() ) {
725 return;
726 }
727
728 // Check if URI is forced cache.
729 $excludes = $this->conf( Base::O_CACHE_FORCE_URI );
730 $hit = Utility::str_hit_array( $req_uri, $excludes, true );
731 if ( $hit ) {
732 list( $result, $this_ttl ) = $hit;
733 self::force_cacheable();
734 self::debug( 'Forced cacheable due to setting: ' . $result );
735 if ( $this_ttl ) {
736 self::set_custom_ttl( $this_ttl );
737 }
738 }
739
740 // if is not cacheable, terminate check.
741 // Even no need to run 3rd party hook.
742 if ( ! self::is_cacheable() ) {
743 self::debug( 'not cacheable before ctrl finalize' );
744 return;
745 }
746
747 // Apply 3rd party filter.
748 // NOTE: Hook always needs to run asap because some 3rd party set is_mobile in this hook.
749 do_action( 'litespeed_control_finalize', defined( 'LSCACHE_IS_ESI' ) ? LSCACHE_IS_ESI : false ); // Pass ESI block id.
750
751 // if is not cacheable, terminate check.
752 if ( ! self::is_cacheable() ) {
753 self::debug( 'not cacheable after api_control' );
754 return;
755 }
756
757 // Check litespeed setting to set cacheable status.
758 if ( ! $this->_setting_cacheable() ) {
759 self::set_nocache();
760 return;
761 }
762
763 // If user has password cookie, do not cache (moved from vary).
764 global $post;
765 if ( ! empty( $post->post_password ) && isset( $_COOKIE[ 'wp-postpass_' . COOKIEHASH ] ) ) {
766 self::set_nocache( 'pswd cookie' );
767 return;
768 }
769
770 // The following check to the end is ONLY for mobile.
771 $is_mobile_conf = apply_filters( 'litespeed_is_mobile', false );
772 if ( ! $this->conf( Base::O_CACHE_MOBILE ) ) {
773 if ( $is_mobile_conf ) {
774 self::set_nocache( 'mobile' );
775 }
776 return;
777 }
778
779 $env_vary = isset( $_SERVER['LSCACHE_VARY_VALUE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['LSCACHE_VARY_VALUE'] ) ) : '';
780 if ( !$env_vary && isset( $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] ) ) {
781 $env_vary = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_LSCACHE_VARY_VALUE'] ) );
782 }
783 if ( $env_vary && false !== strpos( $env_vary, 'ismobile' ) ) {
784 if ( ! wp_is_mobile() && ! $is_mobile_conf ) {
785 self::set_nocache( 'is not mobile' ); // todo: no need to uncache, it will correct vary value in vary finalize anyways.
786 return;
787 }
788 } elseif ( wp_is_mobile() || $is_mobile_conf ) {
789 self::set_nocache( 'is mobile' );
790 return;
791 }
792 }
793
794 /**
795 * Check if is mobile for filter `litespeed_is_mobile` in API.
796 *
797 * @since 3.0
798 * @access public
799 * @return bool
800 */
801 public static function is_mobile() {
802 return wp_is_mobile();
803 }
804
805 /**
806 * Get request method w/ compatibility to X-Http-Method-Override.
807 *
808 * @since 6.2
809 * @return string
810 */
811 private function _get_req_method() {
812 if ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
813 $override = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) );
814 self::debug( 'X-Http-Method-Override -> ' . $override );
815 if ( ! defined( 'LITESPEED_X_HTTP_METHOD_OVERRIDE' ) ) {
816 define( 'LITESPEED_X_HTTP_METHOD_OVERRIDE', true );
817 }
818 return $override;
819 }
820 if ( isset( $_SERVER['REQUEST_METHOD'] ) ) {
821 return sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) );
822 }
823 return 'unknown';
824 }
825
826 /**
827 * Check if a page is cacheable based on litespeed setting.
828 *
829 * @since 1.0.0
830 * @access private
831 * @return bool True if cacheable, false otherwise.
832 */
833 private function _setting_cacheable() {
834 // logged_in users already excluded, no hook added.
835
836 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
837 if ( ! empty( $_REQUEST[ Router::ACTION ] ) ) {
838 return $this->_no_cache_for( 'Query String Action' );
839 }
840
841 $method = $this->_get_req_method();
842 if ( defined( 'LITESPEED_X_HTTP_METHOD_OVERRIDE' ) && LITESPEED_X_HTTP_METHOD_OVERRIDE && 'HEAD' === $method ) {
843 return $this->_no_cache_for( 'HEAD method from override' );
844 }
845 if ( 'GET' !== $method && 'HEAD' !== $method ) {
846 return $this->_no_cache_for( 'Not GET method: ' . $method );
847 }
848
849 if ( is_feed() && 0 === $this->conf( Base::O_CACHE_TTL_FEED ) ) {
850 return $this->_no_cache_for( 'feed' );
851 }
852
853 if ( is_trackback() ) {
854 return $this->_no_cache_for( 'trackback' );
855 }
856
857 if ( is_search() ) {
858 return $this->_no_cache_for( 'search' );
859 }
860
861 // Check private cache URI setting.
862 $excludes = $this->conf( Base::O_CACHE_PRIV_URI );
863 $req_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
864 $result = Utility::str_hit_array( $req_uri, $excludes );
865 if ( $result ) {
866 self::set_private( 'Admin cfg Private Cached URI: ' . $result );
867 }
868
869 if ( ! self::is_forced_cacheable() ) {
870 // Check if URI is excluded from cache.
871 $excludes = $this->cls( 'Data' )->load_cache_nocacheable( $this->conf( Base::O_CACHE_EXC ) );
872 $result = Utility::str_hit_array( $req_uri, $excludes );
873 if ( $result ) {
874 return $this->_no_cache_for( 'Admin configured URI Do not cache: ' . $result );
875 }
876
877 // Check QS excluded setting.
878 $excludes = $this->conf( Base::O_CACHE_EXC_QS );
879 $qs_hit = $this->_is_qs_excluded( $excludes );
880 if ( ! empty( $excludes ) && $qs_hit ) {
881 return $this->_no_cache_for( 'Admin configured QS Do not cache: ' . $qs_hit );
882 }
883
884 $excludes = $this->conf( Base::O_CACHE_EXC_CAT );
885 if ( ! empty( $excludes ) && has_category( $excludes ) ) {
886 return $this->_no_cache_for( 'Admin configured Category Do not cache.' );
887 }
888
889 $excludes = $this->conf( Base::O_CACHE_EXC_TAG );
890 if ( ! empty( $excludes ) && has_tag( $excludes ) ) {
891 return $this->_no_cache_for( 'Admin configured Tag Do not cache.' );
892 }
893
894 $excludes = $this->conf( Base::O_CACHE_EXC_COOKIES );
895 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- names only, compared as keys.
896 if ( ! empty( $excludes ) && ! empty( $_COOKIE ) ) {
897 $cookie_hit = array_intersect( array_keys( $_COOKIE ), $excludes );
898 if ( $cookie_hit ) {
899 return $this->_no_cache_for( 'Admin configured Cookie Do not cache.' );
900 }
901 }
902
903 $excludes = $this->conf( Base::O_CACHE_EXC_USERAGENTS );
904 if ( ! empty( $excludes ) && isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
905 $nummatches = preg_match( Utility::arr2regex( $excludes ), sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) );
906 if ( $nummatches ) {
907 return $this->_no_cache_for( 'Admin configured User Agent Do not cache.' );
908 }
909 }
910
911 // Check if is exclude roles ( Need to set Vary too ).
912 $result = $this->in_cache_exc_roles();
913 if ( $result ) {
914 return $this->_no_cache_for( 'Role Excludes setting ' . $result );
915 }
916 }
917
918 return true;
919 }
920
921 /**
922 * Write a debug message for if a page is not cacheable.
923 *
924 * @since 1.0.0
925 * @access private
926 *
927 * @param string $reason An explanation for why the page is not cacheable.
928 * @return bool Always false.
929 */
930 private function _no_cache_for( $reason ) {
931 self::debug( 'X Cache_control off - ' . $reason );
932 return false;
933 }
934
935 /**
936 * Check if current request has qs excluded setting.
937 *
938 * @since 1.3
939 * @access private
940 *
941 * @param array<int,string> $excludes QS excludes setting.
942 * @return bool|string False if not excluded, otherwise the hit qs list.
943 */
944 private function _is_qs_excluded( $excludes ) {
945 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
946 if ( ! empty( $_GET ) ) {
947 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
948 $keys = array_keys( $_GET );
949 $intersect = array_intersect( $keys, $excludes );
950 if ( $intersect ) {
951 return implode( ',', $intersect );
952 }
953 }
954 return false;
955 }
956 }
957