PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / 3.7.3
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score v3.7.3
trunk 1.0 1.0.1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1 2.1.1 2.1.2 2.2 2.2.1 All 69 releases
powered-cache / includes / utils.php

utils.php in Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score 3.7.3, at includes/utils.php

1,597 lines 40.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Utils
4 *
5 * @package PoweredCache
6 */
7
8 namespace PoweredCache\Utils;
9
10 use PoweredCache\Encryption;
11 use const PoweredCache\Constants\SETTING_OPTION;
12 use RecursiveDirectoryIterator;
13 use RecursiveIteratorIterator;
14
15 /**
16 * Is plugin activated network wide?
17 *
18 * @param string $plugin_file file path
19 *
20 * @return bool
21 * @since 2.0
22 */
23 function is_network_wide( $plugin_file ) {
24 if ( ! is_multisite() ) {
25 return false;
26 }
27
28 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
29 require_once ABSPATH . '/wp-admin/includes/plugin.php';
30 }
31
32 return is_plugin_active_for_network( plugin_basename( $plugin_file ) );
33 }
34
35 /**
36 * Get settings with defaults
37 *
38 * @param bool $force_network_wide Whether getting settings for network or not.
39 * The function respects `POWERED_CACHE_IS_NETWORK` by default.
40 * However, `POWERED_CACHE_IS_NETWORK` is not functional on
41 * (de)activation hooks.
42 *
43 * @return array
44 * @since 2.0
45 */
46 function get_settings( $force_network_wide = false ) {
47 global $is_apache;
48
49 $settings = [
50 // basic options
51 'enable_page_cache' => true,
52 'object_cache' => 'off',
53 'cache_mobile' => true,
54 'cache_mobile_separate_file' => false,
55 'loggedin_user_cache' => false,
56 'ssl_cache' => true, // deprecated
57 'gzip_compression' => false,
58 'cache_timeout' => 1440,
59 // advanced options
60 'auto_configure_htaccess' => $is_apache,
61 'rejected_user_agents' => '',
62 'rejected_cookies' => '',
63 'rejected_referrers' => '',
64 'vary_cookies' => '',
65 'rejected_uri' => '',
66 'ignored_query_strings' => '',
67 'cache_query_strings' => '',
68 'purge_additional_pages' => '',
69 // file optimization
70 'minify_html' => false,
71 'minify_html_dom_optimization' => false,
72 'combine_google_fonts' => false,
73 'swap_google_fonts_display' => true,
74 'use_bunny_fonts' => false,
75 'minify_css' => false,
76 'combine_css' => false,
77 'critical_css' => false,
78 'critical_css_additional_files' => '',
79 'critical_css_excluded_files' => '',
80 'critical_css_appended_content' => '',
81 'critical_css_fallback' => '',
82 'excluded_css_files' => '',
83 'remove_unused_css' => false,
84 'ucss_safelist' => '',
85 'ucss_excluded_files' => '',
86 'minify_js' => false,
87 'combine_js' => false,
88 'excluded_js_files' => '',
89 'js_execution_method' => 'blocking', // deprecated @since 3.2
90 'js_defer' => false,
91 'js_defer_exclusions' => '',
92 'js_delay' => false,
93 'js_delay_exclusions' => '',
94 'js_delay_timeout' => 0,
95 'js_execution_optimized_only' => true, // deprecated @since 3.2
96 'rewrite_file_optimizer' => $is_apache,
97 // media optimization
98 'enable_image_optimization' => false,
99 'image_optimizer_preferred_format' => '',
100 'add_missing_image_dimensions' => false,
101 // lazyload
102 'enable_lazy_load' => false,
103 'lazy_load_post_content' => true,
104 'lazy_load_images' => true,
105 'lazy_load_iframes' => true,
106 'lazy_load_widgets' => true,
107 'lazy_load_post_thumbnail' => true,
108 'lazy_load_avatars' => true,
109 'lazy_load_youtube' => false,
110 'lazy_load_skip_first_nth_img' => 3,
111 'lazy_load_exclusions' => '',
112 'disable_wp_lazy_load' => false,
113 'disable_wp_embeds' => false,
114 'disable_emoji_scripts' => false,
115 // cdn
116 'enable_cdn' => false,
117 'cdn_hostname' => array( '' ),
118 'cdn_zone' => array( '' ),
119 'cdn_rejected_files' => '',
120 // preload
121 'enable_cache_preload' => false,
122 'preload_homepage' => true,
123 'preload_public_posts' => true,
124 'preload_public_tax' => true,
125 'enable_sitemap_preload' => false,
126 'preload_request_interval' => 2, // in seconds
127 'preload_sitemap' => '',
128 'prefetch_dns' => '',
129 'preconnect_resource' => '',
130 'prefetch_links' => true,
131 'enable_lcp_optimization' => false,
132 // db options
133 'db_cleanup_post_revisions' => false,
134 'db_cleanup_auto_drafts' => false,
135 'db_cleanup_trashed_posts' => false,
136 'db_cleanup_spam_comments' => false,
137 'db_cleanup_trashed_comments' => false,
138 'db_cleanup_expired_transients' => false,
139 'db_cleanup_all_transients' => false,
140 'db_cleanup_optimize_tables' => false,
141 'enable_scheduled_db_cleanup' => false,
142 'scheduled_db_cleanup_frequency' => 'daily',
143 // add-ons
144 'enable_cloudflare' => false,
145 'cloudflare_api_token' => '',
146 'cloudflare_email' => '',
147 'cloudflare_api_key' => '',
148 'cloudflare_zone' => '',
149 'enable_heartbeat' => false, // extension status
150 'heartbeat_dashboard_status' => 'enable', // enable,disable,modify
151 'heartbeat_dashboard_interval' => 60, // default interval in seconds
152 'heartbeat_editor_status' => 'enable', // enable,disable,modify
153 'heartbeat_editor_interval' => 15, // default interval in seconds
154 'heartbeat_frontend_status' => 'enable', // enable,disable,modify
155 'heartbeat_frontend_interval' => 60, // default interval in seconds
156 'enable_varnish' => false,
157 'varnish_ip' => '',
158 // misc
159 'cache_footprint' => true,
160 'async_cache_cleaning' => false,
161 'dev_mode' => false,
162 // new options needs to migrate from extensions
163 'enable_google_tracking' => false,
164 'enable_fb_tracking' => false,
165 ];
166
167 /**
168 * Filter default settings.
169 *
170 * @hook powered_cache_default_settings
171 *
172 * @param {array} $settings Default settings.
173 *
174 * @return {array} New value
175 * @since 2.0
176 */
177 $default_settings = apply_filters( 'powered_cache_default_settings', $settings );
178
179 if ( POWERED_CACHE_IS_NETWORK || $force_network_wide ) {
180 $settings = get_site_option( SETTING_OPTION, [] );
181 } else {
182 $settings = get_option( SETTING_OPTION, [] );
183 }
184
185 $settings = wp_parse_args( $settings, $default_settings );
186
187 return $settings;
188 }
189
190
191 /**
192 * return base caching dir
193 * use this function to get base caching directory instead of directly calling constant
194 *
195 * @return string path
196 * @since 1.0
197 */
198 function get_cache_dir() {
199 if ( defined( 'POWERED_CACHE_CACHE_DIR' ) ) {
200 return POWERED_CACHE_CACHE_DIR; // don't change unless have a particular reason
201 }
202
203 return WP_CONTENT_DIR . '/cache/';
204 }
205
206
207 /**
208 * Object cache methods keys will use as option
209 *
210 * @return array $object_caches
211 * @since 1.2 apcu added
212 * @since 1.0
213 */
214 function get_object_cache_dropins() {
215
216 $object_caches = array(
217 'memcache' => POWERED_CACHE_DROPIN_DIR . 'memcache-object-cache.php',
218 'memcached' => POWERED_CACHE_DROPIN_DIR . 'memcached-object-cache.php',
219 'redis' => POWERED_CACHE_DROPIN_DIR . 'redis-object-cache.php',
220 'apcu' => POWERED_CACHE_DROPIN_DIR . 'apcu-object-cache.php',
221 );
222
223 /**
224 * Filter object cache dropins.
225 *
226 * @hook powered_cache_object_cache_dropins
227 *
228 * @param {array} $object_caches The list of supported object-cache dropins.
229 *
230 * @return {array} New value
231 * @since 1.0
232 */
233 return apply_filters( 'powered_cache_object_cache_dropins', $object_caches );
234 }
235
236
237 /**
238 * Get available object cache backends
239 *
240 * @return array
241 * @since 1.2 unset apcu
242 * @since 1.0
243 */
244 function get_available_object_caches() {
245 $object_cache_methods = get_object_cache_dropins();
246
247 if ( ! class_exists( '\Memcache' ) || version_compare( PHP_VERSION, '5.6.20', '<' ) ) {
248 unset( $object_cache_methods['memcache'] );
249 }
250
251 if ( ! class_exists( '\Memcached' ) ) {
252 unset( $object_cache_methods['memcached'] );
253 }
254
255 if ( ! class_exists( '\Redis' ) ) {
256 unset( $object_cache_methods['redis'] );
257 }
258
259 if ( ! function_exists( '\apcu_add' ) ) {
260 unset( $object_cache_methods['apcu'] );
261 }
262
263 return array_keys( $object_cache_methods );
264 }
265
266
267 /**
268 * convert minutes to possible time format
269 *
270 * @param int $timeout_in_minutes TTL in minutes
271 *
272 * @return array
273 * @since 1.1
274 */
275 function get_timeout_with_interval( $timeout_in_minutes ) {
276 $cache_timeout = $timeout_in_minutes;
277 $selected_interval = 'MINUTE';
278
279 if ( $cache_timeout > 0 ) {
280 if ( 0 === (int) ( $cache_timeout % 1440 ) ) {
281 $cache_timeout = $cache_timeout / 1440;
282 $selected_interval = 'DAY';
283 } elseif ( 0 === (int) ( $cache_timeout % 60 ) ) {
284 $cache_timeout = $cache_timeout / 60;
285 $selected_interval = 'HOUR';
286 }
287 }
288
289 return array(
290 $cache_timeout,
291 $selected_interval,
292 );
293 }
294
295 /**
296 * Determine whether display or not display htaccess configuration
297 * .htaccess can affect the way of serving cached files.
298 * Therefore it's only available for network admin on multisite
299 *
300 * @return bool
301 */
302 function can_configure_htaccess() {
303 global $is_apache;
304
305 if ( ! $is_apache ) {
306 return false;
307 }
308
309 if ( POWERED_CACHE_IS_NETWORK && current_user_can( 'manage_network' ) ) {
310 return true;
311 }
312
313 if ( is_multisite() && ! POWERED_CACHE_IS_NETWORK ) {
314 return false;
315 }
316
317 if ( current_user_can( 'manage_options' ) ) {
318 return true;
319 }
320
321 return false;
322 }
323
324 /**
325 * Whether current user capable to do any configuration changes
326 *
327 * @return bool
328 */
329 function can_control_all_settings() {
330 if ( is_multisite() ) {
331 if ( current_user_can( 'manage_network' ) ) {
332 return true;
333 }
334
335 return false;
336 }
337
338 if ( current_user_can( 'manage_options' ) ) {
339 return true;
340 }
341
342 return false;
343 }
344
345
346 /**
347 * Object cache has an effect on all WP
348 * So, it should be available for the network admin on multisite
349 * regardless of network-wide or individual activated
350 *
351 * @return bool
352 */
353 function can_configure_object_cache() {
354 if ( is_multisite() ) {
355 // only allow on network-wide activation
356 if ( POWERED_CACHE_IS_NETWORK && current_user_can( 'manage_network' ) ) {
357 return true;
358 }
359
360 return false;
361 }
362
363 if ( current_user_can( 'manage_options' ) ) {
364 return true;
365 }
366
367 return false;
368 }
369
370 /**
371 * Supported js execution methods
372 *
373 * @depreacated since 3.2
374 *
375 * @return mixed|void
376 */
377 function js_execution_methods() {
378 $methods = [
379 'blocking' => esc_html__( 'Blocking – (default)', 'powered-cache' ),
380 'async' => esc_html__( 'Non-blocking using async', 'powered-cache' ),
381 'defer' => esc_html__( 'Non-blocking using defer', 'powered-cache' ),
382 'delayed' => esc_html__( 'Delayed for user interaction', 'powered-cache' ),
383 ];
384
385 /**
386 * Filter supported JS execution methods.
387 *
388 * @hook powered_cache_js_execution_methods
389 *
390 * @param {array} $powered_cache_js_execution_methods JS execution methods.
391 *
392 * @return {array} New value
393 * @since 2.0
394 */
395 return apply_filters( 'powered_cache_js_execution_methods', $methods );
396 }
397
398
399 /**
400 * Get available zones
401 *
402 * @return mixed|void
403 * @since 1.0
404 */
405 function cdn_zones() {
406 $zones = [
407 'all' => esc_html__( 'All files', 'powered-cache' ),
408 'image' => esc_html__( 'Images', 'powered-cache' ),
409 'js' => esc_html__( 'JavaScript', 'powered-cache' ),
410 'css' => esc_html__( 'CSS', 'powered-cache' ),
411 ];
412
413 /**
414 * Filter CDN zone options.
415 *
416 * @hook powered_cache_cdn_zones
417 *
418 * @param {array} $zones CDN Zones (all,image,js,css)
419 *
420 * @return {array} New value
421 * @since 1.0
422 */
423 return apply_filters( 'powered_cache_cdn_zones', $zones );
424 }
425
426 /**
427 * Which version of plugin running
428 *
429 * @return bool
430 */
431 function is_premium() {
432 if ( defined( 'POWERED_CACHE_PREMIUM_PLUGIN_FILE' ) && POWERED_CACHE_PREMIUM_PLUGIN_FILE ) {
433 return true;
434 }
435
436 return false;
437 }
438
439 /**
440 * Scheduled cleanup options
441 *
442 * @return array
443 */
444 function scheduled_cleanup_frequency_options() {
445 $options = [
446 'daily' => esc_html__( 'Daily', 'powered-cache' ),
447 'weekly' => esc_html__( 'Weekly', 'powered-cache' ),
448 'monthly' => esc_html__( 'Monthly', 'powered-cache' ),
449 ];
450
451 /**
452 * Filter scheduled cleanup options.
453 *
454 * @hook powered_cache_scheduled_cleanup_frequency_options
455 *
456 * @param {array} $options The list of supported schedules.
457 *
458 * @return {array} New value
459 * @since 2.0
460 */
461 return apply_filters( 'powered_cache_scheduled_cleanup_frequency_options', $options );
462 }
463
464 /**
465 * ports \settings_errors for SUI
466 *
467 * @param string $setting Slug title of a specific setting
468 * @param bool $sanitize Whether to re-sanitize the setting value before returning errors
469 * @param bool $hide_on_update Whether hide or not hide on update
470 *
471 * @see settings_errors
472 */
473 function settings_errors( $setting = '', $sanitize = false, $hide_on_update = false ) {
474
475 if ( $hide_on_update && ! empty( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
476 return;
477 }
478
479 $settings_errors = get_settings_errors( $setting, $sanitize );
480
481 if ( empty( $settings_errors ) ) {
482 return;
483 }
484
485 $output = '';
486
487 foreach ( $settings_errors as $key => $details ) {
488 if ( 'updated' === $details['type'] ) {
489 $details['type'] = 'sui-notice-success';
490 }
491
492 if ( in_array( $details['type'], array( 'error', 'success', 'warning', 'info' ), true ) ) {
493 $details['type'] = 'sui-notice-' . $details['type'];
494 }
495
496 $css_id = sprintf(
497 'setting-error-%s',
498 esc_attr( $details['code'] )
499 );
500
501 $css_class = sprintf(
502 'sui-notice %s settings-error is-dismissible',
503 esc_attr( $details['type'] )
504 );
505
506 $output .= "<div id='$css_id' class='$css_class'> \n";
507 $output .= "<div class='sui-notice-content'><div class='sui-notice-message'>";
508 $output .= "<span class='sui-notice-icon sui-icon-info sui-md' aria-hidden='true'></span>";
509 $output .= "<p>{$details['message']}</p></div></div>";
510 $output .= "</div> \n";
511 }
512
513 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
514 }
515
516 /**
517 * remove directories recursively
518 * Adopted from W3TC Utility
519 *
520 * @param string $path The target path
521 * @param array $exclude list of the files that will excluded
522 *
523 * @return void
524 * @since 1.2.5
525 */
526 function remove_dir( $path, $exclude = array() ) {
527 // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
528 $dir = @opendir( $path );
529
530 if ( $dir ) {
531 while ( ( $entry = @readdir( $dir ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
532 if ( '.' === $entry || '..' === $entry ) {
533 continue;
534 }
535
536 foreach ( $exclude as $mask ) {
537 if ( fnmatch( $mask, basename( $entry ) ) ) {
538 continue 2;
539 }
540 }
541
542 $full_path = $path . DIRECTORY_SEPARATOR . $entry;
543
544 if ( @is_dir( $full_path ) ) {
545 remove_dir( $full_path, $exclude );
546 } else {
547 @unlink( $full_path );
548 }
549 }
550
551 @closedir( $dir );
552 @rmdir( $path );
553 }
554 // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
555 }
556
557 /**
558 * Get base caching directory of the site.
559 *
560 * @return mixed|void
561 * @since 1.1
562 */
563 function site_cache_dir() {
564 $base_dir = get_page_cache_dir();
565
566 // compatible with multisite
567 $site_url = get_site_url();
568
569 $site_url_parsed = wp_parse_url( $site_url );
570
571 $site_path = $site_url_parsed['host'];
572
573 if ( ! empty( $site_url_parsed['path'] ) ) {
574 $site_path .= $site_url_parsed['path'];
575 }
576
577 $site_cache_dir = trailingslashit( $base_dir . $site_path );
578
579 /**
580 * Filter get base caching directory of site
581 *
582 * @hook powered_cache_site_cache_dir
583 *
584 * @param {string} $site_cache_dir Site cache dir.
585 *
586 * @return {string} New value
587 * @since 1.1
588 */
589 return apply_filters( 'powered_cache_site_cache_dir', $site_cache_dir );
590 }
591
592 /**
593 * Page cache base directory.
594 *
595 * @return string
596 * @since 1.1 $url parameter removed
597 * @since 1.0
598 */
599 function get_page_cache_dir() {
600 $path = get_cache_dir() . 'powered-cache/';
601
602 /**
603 * Filter page cache base directory.
604 *
605 * @hook powered_cache_get_page_cache_dir
606 *
607 * @param {string} $path Page cache dir
608 *
609 * @return {string} New value
610 * @since 1.0
611 */
612 return apply_filters( 'powered_cache_get_page_cache_dir', $path );
613 }
614
615 /**
616 * Clean up cache directory
617 *
618 * @return mixed
619 * @since 1.0
620 */
621 function clean_page_cache_dir() {
622 remove_dir( get_page_cache_dir() );
623 }
624
625 /**
626 * Clean cache base for the current site
627 *
628 * @return mixed
629 * @since 1.1
630 */
631 function clean_site_cache_dir() {
632 $site_cache_dir = site_cache_dir();
633
634 /**
635 * When deleting cache for the main site on multisite subdirectory setup
636 * Don't delete other site's cache
637 */
638 if ( is_multisite() && ! is_subdomain_install() && is_main_site() ) {
639 $base_dir = get_page_cache_dir();
640 $directories = glob( $site_cache_dir . '*', GLOB_ONLYDIR );
641 $site_url = get_site_url();
642
643 $site_domain = wp_parse_url( $site_url, PHP_URL_HOST );
644 foreach ( $directories as $directory ) {
645 $dir_name = str_replace( $base_dir . $site_domain, '', $directory );
646 $site_info = get_site_by_path( $site_domain, $dir_name );
647 if ( ! $site_info || is_main_site( $site_info->blog_id ) ) {
648 remove_dir( $directory );
649 }
650 }
651 } else {
652 remove_dir( $site_cache_dir );
653 }
654
655 /**
656 * Fires after deleting site cache dir
657 *
658 * @hook powered_cache_clean_site_cache_dir
659 *
660 * @param {string} $site_cache_dir The caching directory of the current site.
661 *
662 * @since 2.0
663 */
664 do_action( 'powered_cache_clean_site_cache_dir', $site_cache_dir );
665 }
666
667
668 /**
669 * Supported mobile browsers
670 *
671 * @return mixed|void
672 * @since 1.0
673 */
674 function mobile_browsers() {
675 $mobile_browsers
676 = '2.0 MMP, 240x320, 400X240, AvantGo, BlackBerry, Blazer, Cellphone, Danger, DoCoMo, Elaine/3.0, EudoraWeb, Googlebot-Mobile, hiptop, IEMobile, KYOCERA/WX310K, LG/U990, MIDP-2., MMEF20, MOT-V, NetFront, Newt, Nintendo Wii, Nitro, Nokia, Opera Mini, Palm, PlayStation Portable, portalmmm, Proxinet, ProxiNet, SHARP-TQ-GX10, SHG-i900, Small, SonyEricsson, Symbian OS, SymbianOS, TS21i-10, UP.Browser, UP.Link, webOS, Windows CE, WinWAP, YahooSeeker/M1A1-R2D2, iPhone, iPod, Android, BlackBerry9530, LG-TU915 Obigo, LGE VX, webOS, Nokia5800';
677
678 /**
679 * Filters supported mobile browsers.
680 *
681 * @hook powered_cache_mobile_browsers
682 *
683 * @param {string} $mobile_browsers Comma separated list of the defined mobile browsers.
684 *
685 * @since 1.0
686 */
687 return apply_filters( 'powered_cache_mobile_browsers', $mobile_browsers );
688 }
689
690 /**
691 * Supported mobile prefixes
692 *
693 * @return mixed|void
694 * @since 1.0
695 */
696 function mobile_prefixes() {
697 $mobile_prefixes
698 = 'w3c , w3c-, acs-, alav, alca, amoi, audi, avan, benq, bird, blac, blaz, brew, cell, cldc, cmd-, dang, doco, eric, hipt, htc_, inno, ipaq, ipod, jigs, kddi, keji, leno, lg-c, lg-d, lg-g, lge-, lg/u, maui, maxo, midp, mits, mmef, mobi, mot-, moto, mwbp, nec-, newt, noki, palm, pana, pant, phil, play, port, prox, qwap, sage, sams, sany, sch-, sec-, send, seri, sgh-, shar, sie-, siem, smal, smar, sony, sph-, symb, t-mo, teli, tim-, tosh, tsm-, upg1, upsi, vk-v, voda, wap-, wapa, wapi, wapp, wapr, webc, winw, winw, xda , xda-';
699
700 /**
701 * Filters supported mobile prefixes.
702 *
703 * @hook powered_cache_mobile_prefixes
704 *
705 * @param {string} $mobile_prefixes Comma separated list of the defined mobile prefixes.
706 *
707 * @since 1.0
708 */
709 return apply_filters( 'powered_cache_mobile_prefixes', $mobile_prefixes );
710 }
711
712
713 /**
714 * Collect post related urls
715 *
716 * @param int $post_id Post ID
717 *
718 * @return array
719 * @since 1.0
720 * @since 1.1 powered_cache_post_related_urls filter added
721 */
722 function get_post_related_urls( $post_id ) {
723 // Valid post statuses that require cache purging.
724 $valid_post_statuses = [ 'publish', 'private', 'trash', 'pending', 'draft' ];
725 $post_status = get_post_status( $post_id );
726 $post = get_post( $post_id );
727
728 // Post types that should not have their cache purged.
729 $excluded_post_types = [ 'nav_menu_item', 'revision' ];
730 $post_type = get_post_type( $post_id );
731 $rest_api_route = 'wp/v2';
732
733 $related_urls = [];
734
735 if ( false !== get_permalink( $post_id ) && in_array( $post_status, $valid_post_statuses, true ) && ! in_array( $post_type, $excluded_post_types, true ) ) {
736 // Add the post URL.
737 $related_urls[] = get_permalink( $post_id );
738
739 // Add REST API URL if applicable.
740 if ( $rest_api_route ) {
741 $post_type_object = get_post_type_object( $post_type );
742 if ( ! empty( $post_type_object->show_in_rest ) ) {
743 $post_type_base = $post_type_object->rest_base ? $post_type_object->rest_base : $post_type_object->name;
744 $related_urls[] = get_rest_url() . $rest_api_route . '/' . $post_type_base . '/' . $post_id . '/';
745 } elseif ( in_array( $post_type, [ 'post', 'page' ], true ) ) {
746 $related_urls[] = get_rest_url() . $rest_api_route . '/' . $post_type . 's/' . $post_id . '/';
747 }
748 }
749
750 // Add AMP URL if AMP plugin is active.
751 if ( function_exists( 'amp_get_permalink' ) ) {
752 $related_urls[] = amp_get_permalink( $post_id );
753 }
754
755 // Regular AMP url for posts if ant of the following are active:
756 // https://wordpress.org/plugins/accelerated-mobile-pages/
757 if ( defined( 'AMPFORWP_AMP_QUERY_VAR' ) ) {
758 $related_urls[] = get_permalink( $post_id ) . 'amp/';
759 }
760
761 // Handle trashed post URLs.
762 if ( 'trash' === $post_status ) {
763 $trash_permalink = str_replace( '__trashed', '', get_permalink( $post_id ) );
764 $related_urls[] = $trash_permalink;
765 $related_urls[] = $trash_permalink . 'feed/';
766 }
767
768 $taxonomies = get_object_taxonomies( get_post_type( $post_id ), 'objects' );
769
770 // Purge terms associated with the post.
771 foreach ( $taxonomies as $taxonomy ) {
772 // Skip non-public taxonomies.
773 if ( ! $taxonomy->public ) {
774 continue;
775 }
776
777 $terms = get_the_terms( $post_id, $taxonomy->name );
778
779 if ( empty( $terms ) || is_wp_error( $terms ) ) {
780 continue;
781 }
782
783 foreach ( $terms as $term ) {
784 $term_url = get_term_link( $term->slug, $taxonomy->name );
785 if ( ! is_wp_error( $term_url ) ) {
786 $related_urls[] = $term_url;
787 if ( $taxonomy->show_in_rest ) {
788 $taxonomy_base = $taxonomy->rest_base ? $taxonomy->rest_base : $taxonomy->name;
789 $related_urls[] = rest_url( "{$taxonomy->rest_namespace}/{$taxonomy_base}/{$term->term_id}/" ); // REST API URL for the term
790 }
791 }
792
793 if ( ! is_taxonomy_hierarchical( $taxonomy->name ) ) {
794 continue;
795 }
796
797 $ancestors = (array) get_ancestors( $term->term_id, $taxonomy->name );
798 foreach ( $ancestors as $ancestor ) {
799 $ancestor_object = get_term( $ancestor, $taxonomy->name );
800 if ( ! is_a( $ancestor, '\WP_Term' ) ) {
801 continue;
802 }
803
804 $ancestor_term_url = get_term_link( $ancestor_object->slug, $taxonomy->name );
805 if ( ! is_wp_error( $ancestor_term_url ) ) {
806 $related_urls[] = $ancestor_term_url;
807 if ( $taxonomy->show_in_rest ) {
808 $taxonomy_base = $taxonomy->rest_base ? $taxonomy->rest_base : $taxonomy->name;
809 $related_urls[] = rest_url( "{$taxonomy->rest_namespace}/{$taxonomy_base}/{$ancestor_object->term_id}/" ); // REST API URL for the ancestor term
810 }
811 }
812 }
813 }
814 }
815
816 // Purge author and feed URLs for posts.
817 if ( 'post' === $post_type ) {
818 $author_id = get_post_field( 'post_author', $post_id );
819 $related_urls[] = get_author_posts_url( $author_id );
820 $related_urls[] = get_author_feed_link( $author_id );
821 if ( $rest_api_route ) {
822 $related_urls[] = get_rest_url() . $rest_api_route . '/users/' . $author_id . '/';
823 }
824
825 // Include various feed URLs.
826 $feed_urls = [
827 get_bloginfo_rss( 'rdf_url' ),
828 get_bloginfo_rss( 'rss_url' ),
829 get_bloginfo_rss( 'rss2_url' ),
830 get_bloginfo_rss( 'atom_url' ),
831 get_bloginfo_rss( 'comments_rss2_url' ),
832 get_post_comments_feed_link( $post_id ),
833 ];
834 $related_urls = array_merge( $related_urls, $feed_urls );
835 }
836
837 // Purge archive pages if not excluded.
838 if ( ! in_array( $post_type, [ 'post', 'page' ], true ) ) {
839 $related_urls[] = get_post_type_archive_link( $post_type );
840 $related_urls[] = get_post_type_archive_feed_link( $post_type );
841 }
842
843 $post_date = strtotime( $post->post_date );
844
845 if ( $post_date ) {
846 // Generate the date archive URLs
847 $year = gmdate( 'Y', $post_date );
848 $month = gmdate( 'm', $post_date );
849 $day = gmdate( 'd', $post_date );
850
851 $related_urls[] = get_year_link( $year );
852 $related_urls[] = get_month_link( $year, $month );
853 $related_urls[] = get_day_link( $year, $month, $day );
854 }
855
856 // Always purge the home page.
857 $related_urls[] = home_url( '/' );
858
859 // Purge the posts page if it's set to a static page.
860 if ( 'page' === get_option( 'show_on_front' ) ) {
861 $posts_page_id = get_option( 'page_for_posts' );
862 if ( $posts_page_id ) {
863 $related_urls[] = get_permalink( $posts_page_id );
864 }
865 }
866 }
867
868 // Remove query strings and ensure unique URLs before purging.
869 $related_urls = array_unique(
870 array_map(
871 function ( $url ) {
872 return strtok( $url, '?' );
873 },
874 $related_urls
875 )
876 );
877
878 /**
879 * Filters post related urls.
880 *
881 * @hook powered_cache_post_related_urls
882 *
883 * @param {array} $related_urls The list of the URLs that related with the post.
884 *
885 * @return {array} New value.
886 * @since 1.0
887 */
888 $related_urls = apply_filters( 'powered_cache_post_related_urls', $related_urls );
889
890 return $related_urls;
891 }
892
893
894 /**
895 * Delete cache file
896 *
897 * @param string $url Target URL
898 * @param bool $delete_subdirectories Whether delete subdirectories or not
899 *
900 * @return bool true when found cache dir, otherwise false
901 * @since 1.0
902 */
903 function delete_page_cache( $url, $delete_subdirectories = false ) {
904 $dir = get_url_dir( trim( $url ) );
905
906 if ( is_dir( $dir ) ) {
907 $files = scandir( $dir );
908 foreach ( $files as $file ) {
909 /**
910 * Don't need to lookup for index-https, index-https-mobile etc..
911 * Just clean that directory's files only.
912 */
913 if ( ! is_dir( $dir . $file ) && file_exists( $dir . $file ) && ! in_array( $file, array( '.', '..' ), true ) ) {
914 unlink( $dir . $file );
915 }
916 }
917
918 if ( file_exists( $dir ) && ( $delete_subdirectories || is_dir_empty( $dir ) ) ) {
919 remove_dir( $dir );
920 }
921
922 return true;
923 }
924
925 return false;
926 }
927
928
929 /**
930 * Get cache location of given url
931 *
932 * @param string $url The url to retrieve path
933 *
934 * @return mixed|void
935 * @since 1.1
936 */
937 function get_url_dir( $url ) {
938 $url_info = wp_parse_url( $url );
939 $sub_dir = isset( $url_info['host'] ) ? $url_info['host'] : '';
940
941 if ( ! empty( $url_info['path'] ) ) {
942 $sub_dir .= $url_info['path'];
943 }
944
945 $path = trailingslashit( get_page_cache_dir() ) . ltrim( $sub_dir, '/' );
946 $path = trailingslashit( $path );
947
948 /**
949 * Filters the path of the given url in the cache directory.
950 *
951 * @hook powered_cache_get_url_dir
952 *
953 * @param {string} $path The cache directory of the given URL.
954 *
955 * @since 1.1
956 */
957 return apply_filters( 'powered_cache_get_url_dir', $path );
958 }
959
960 /**
961 * Prepare cdn addresses with hostname + zone
962 *
963 * @return mixed|void
964 * @since 1.0
965 */
966 function cdn_addresses() {
967 $settings = get_settings(); // phpcs:ignore WordPress.WP.DeprecatedFunctions.get_settingsFound
968
969 $hostnames = $settings['cdn_hostname'];
970 $zones = $settings['cdn_zone'];
971
972 $cdn_addresses = array();
973 foreach ( $hostnames as $host_key => $host ) {
974 if ( filter_var( $host, FILTER_VALIDATE_URL ) ) {
975 $host = wp_parse_url( $host, PHP_URL_HOST );
976 }
977
978 if ( ! empty( $host ) ) {
979 $cdn_addresses[ $zones[ $host_key ] ][] = $host;
980 }
981 }
982
983 /**
984 * Filters CDN Addresses.
985 *
986 * @hook powered_cache_cdn_addresses
987 *
988 * @param {array} $cdn_addresses CDN Addresses.
989 *
990 * @return {array} New value.
991 * @since 1.0
992 */
993 return apply_filters( 'powered_cache_cdn_addresses', $cdn_addresses );
994 }
995
996
997 /**
998 * Get list of expired files for given directory
999 *
1000 * @param string $path directory location
1001 * @param int $lifespan lifespan in seconds
1002 *
1003 * @return array expired file list
1004 * @since 1.1
1005 */
1006 function get_expired_files( $path, $lifespan = 0 ) {
1007
1008 $current_time = time();
1009
1010 $expired_files = array();
1011
1012 // return immediately if the path is not exist!
1013 if ( ! file_exists( $path ) ) {
1014 return $expired_files;
1015 }
1016
1017 $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path ) );
1018
1019 foreach ( $files as $file ) {
1020
1021 if ( $file->isDir() ) {
1022 continue;
1023 }
1024
1025 $path = $file->getPathname();
1026
1027 if ( @filemtime( $path ) + $lifespan <= $current_time ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1028 $expired_files[] = $path;
1029 }
1030 }
1031
1032 return $expired_files;
1033 }
1034
1035
1036 /**
1037 * Flush object cache and clean cache directory
1038 *
1039 * @since 1.0
1040 */
1041 function powered_cache_flush() {
1042 if ( function_exists( 'wp_cache_flush' ) ) {
1043 wp_cache_flush();
1044 }
1045
1046 remove_dir( get_cache_dir() );
1047
1048 /**
1049 * Fires after cache flush.
1050 *
1051 * @hook powered_cache_flushed
1052 * @since 1.0
1053 */
1054 do_action( 'powered_cache_flushed' );
1055 }
1056
1057 /**
1058 * Log to stdout or a file
1059 *
1060 * @param string $message Log message
1061 *
1062 * @return bool
1063 */
1064 function log( $message ) {
1065 if ( ! defined( 'POWERED_CACHE_ENABLE_LOG' ) ) {
1066 return false;
1067 }
1068
1069 if ( ! POWERED_CACHE_ENABLE_LOG ) {
1070 return false;
1071 }
1072
1073 $log_message = gmdate( 'H:i:s' ) . ' ' . getmypid() . ' ' . get_client_ip() . " {$message}" . PHP_EOL;
1074
1075 /**
1076 * Filters log message.
1077 *
1078 * @hook powered_cache_log_message
1079 *
1080 * @param {string} $log_message The log message.
1081 *
1082 * @return {string} New value.
1083 * @since 2.0
1084 */
1085 $log_message = apply_filters( 'powered_cache_log_message', $log_message );
1086
1087 /**
1088 * Filters log message type.
1089 *
1090 * @hook powered_cache_log_message_type
1091 *
1092 * @param {int} 0 default message type since 3.6
1093 *
1094 * @return {int} New value.
1095 * @since 2.0
1096 */
1097 $message_type = apply_filters( 'powered_cache_log_message_type', 0 );
1098 $destination = null;
1099
1100 if ( defined( 'POWERED_CACHE_LOG_FILE' ) ) {
1101 $destination = POWERED_CACHE_LOG_FILE;
1102 $message_type = 3;
1103 }
1104
1105 /**
1106 * Filters destination of the log.
1107 *
1108 * @hook powered_cache_log_destination
1109 *
1110 * @param {null|string} $destination The destination of the log.
1111 *
1112 * @return {null|string} New value.
1113 * @since 2.0
1114 */
1115 $log_destination = apply_filters( 'powered_cache_log_destination', $destination );
1116
1117 // don't log anything when it used for particular IP address
1118 if ( defined( 'POWERED_CACHE_LOG_IP' ) && POWERED_CACHE_LOG_IP !== get_client_ip() ) {
1119 return false;
1120 }
1121
1122 $message_type = absint( $message_type );
1123
1124 return error_log( $log_message, $message_type, $log_destination ); // phpcs:ignore
1125 }
1126
1127 /**
1128 * Get client raw ip
1129 *
1130 * @return mixed
1131 */
1132 function get_client_ip() {
1133 if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
1134 return wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1135 }
1136
1137 if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
1138 return wp_unslash( $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1139 }
1140 }
1141
1142 /**
1143 * Fragment caching
1144 *
1145 * @link https://gist.github.com/markjaquith/2653957
1146 * @see https://gist.github.com/westonruter/5475349
1147 *
1148 * @param string $key Fragment key
1149 * @param int $ttl Cache TTL
1150 * @param callable $function callback
1151 *
1152 * @throws \Exception Exception
1153 * @since 1.2
1154 * @since 2.0 Renamed powered_cache_fragment -> \PoweredCache\Utils\cache_fragment
1155 */
1156 function cache_fragment( $key, $ttl, $function ) {
1157 $group = 'powered-fragments';
1158 $output = wp_cache_get( $key, $group );
1159 if ( empty( $output ) ) {
1160 ob_start();
1161 call_user_func( $function );
1162 $output = ob_get_clean();
1163 wp_cache_add( $key, $output, $group, $ttl );
1164 }
1165 echo $output; // phpcs:ignore
1166 }
1167
1168 /**
1169 * Fetches known headers, ported from WP Super Cache but not using apache_response_headers
1170 *
1171 * @return array|false
1172 * @since 1.2
1173 */
1174 function get_response_headers() {
1175 static $known_headers = array(
1176 'Access-Control-Allow-Origin',
1177 'Accept-Ranges',
1178 'Age',
1179 'Allow',
1180 'Cache-Control',
1181 'Connection',
1182 'Content-Encoding',
1183 'Content-Language',
1184 'Content-Length',
1185 'Content-Location',
1186 'Content-MD5',
1187 'Content-Disposition',
1188 'Content-Range',
1189 'Content-Type',
1190 'Date',
1191 'ETag',
1192 'Expires',
1193 'Last-Modified',
1194 'Link',
1195 'Location',
1196 'P3P',
1197 'Pragma',
1198 'Proxy-Authenticate',
1199 'Referrer-Policy',
1200 'Refresh',
1201 'Retry-After',
1202 'Server',
1203 'Status',
1204 'Strict-Transport-Security',
1205 'Trailer',
1206 'Transfer-Encoding',
1207 'Upgrade',
1208 'Vary',
1209 'Via',
1210 'Warning',
1211 'WWW-Authenticate',
1212 'X-Frame-Options',
1213 'Public-Key-Pins',
1214 'X-XSS-Protection',
1215 'Content-Security-Policy',
1216 'X-Pingback',
1217 'X-Content-Security-Policy',
1218 'X-WebKit-CSP',
1219 'X-Content-Type-Options',
1220 'X-Powered-By',
1221 'X-UA-Compatible',
1222 'X-Robots-Tag',
1223 );
1224
1225 /**
1226 * Filters known headers.
1227 *
1228 * @hook powered_cache_known_headers
1229 *
1230 * @param {array} $known_headers The list of known HTTP headers.
1231 *
1232 * @return {array} New value.
1233 * @since 1.2
1234 */
1235 $known_headers = apply_filters( 'powered_cache_known_headers', $known_headers );
1236
1237 if ( ! isset( $known_headers['age'] ) ) {
1238 $known_headers = array_map( 'strtolower', $known_headers );
1239 }
1240
1241 $headers = array();
1242
1243 if ( function_exists( 'headers_list' ) ) {
1244 $headers = array();
1245 foreach ( headers_list() as $hdr ) {
1246 $header_parts = explode( ':', $hdr, 2 );
1247 $header_name = isset( $header_parts[0] ) ? trim( $header_parts[0] ) : '';
1248 $header_value = isset( $header_parts[1] ) ? trim( $header_parts[1] ) : '';
1249
1250 $headers[ $header_name ] = $header_value;
1251 }
1252 }
1253
1254 foreach ( $headers as $key => $value ) {
1255 if ( ! in_array( strtolower( $key ), $known_headers, true ) ) {
1256 unset( $headers[ $key ] );
1257 }
1258 }
1259
1260 return $headers;
1261 }
1262
1263
1264 /**
1265 * Check if the given url exists in the cache
1266 *
1267 * @param string $url URL
1268 * @param bool $is_mobile Check mobile cache
1269 * @param bool $is_gzip check gzipped cache
1270 *
1271 * @return bool
1272 */
1273 function is_url_cached( $url, $is_mobile = false, $is_gzip = false ) {
1274 $file_name = 'index';
1275 $url_parts = wp_parse_url( $url );
1276
1277 if ( 'https' === $url_parts['scheme'] ) {
1278 $file_name .= '-https';
1279 }
1280
1281 if ( $is_mobile ) {
1282 $file_name .= '-mobile';
1283 }
1284
1285 $file_name .= '.html';
1286
1287 if ( $is_gzip ) {
1288 $file_name .= '.gz';
1289 }
1290
1291 $rel_path = $url_parts['host'];
1292 if ( ! empty( $url_parts['path'] ) ) {
1293 $rel_path .= $url_parts['path'];
1294 }
1295
1296 $path = trailingslashit( get_page_cache_dir() . $rel_path );
1297
1298 $cache_file = $path . $file_name;
1299
1300 return file_exists( $cache_file );
1301 }
1302
1303 /**
1304 * Check if the permalink structure of the site end with trailingslash
1305 *
1306 * @return bool
1307 * @since 2.0
1308 */
1309 function permalink_structure_has_trailingslash() {
1310 if ( '/' === substr( get_option( 'permalink_structure' ), - 1 ) ) {
1311 return true;
1312 }
1313
1314 return false;
1315 }
1316
1317 /**
1318 * Check if the given directory empty
1319 *
1320 * @param string $dir Path
1321 *
1322 * @return bool
1323 */
1324 function is_dir_empty( $dir ) {
1325 foreach ( new \DirectoryIterator( $dir ) as $file_info ) {
1326 if ( $file_info->isDot() ) {
1327 continue;
1328 }
1329
1330 return false;
1331 }
1332
1333 return true;
1334 }
1335
1336 /**
1337 * Get the documentation url
1338 *
1339 * @param string $path The path of documentation
1340 * @param string $fragment URL Fragment
1341 *
1342 * @return string final URL
1343 */
1344 function get_doc_url( $path = null, $fragment = '' ) {
1345 $doc_site = 'https://docs.poweredcache.com/';
1346 $utm_parameters = '?utm_source=wp_admin&utm_medium=plugin&utm_campaign=settings_page';
1347
1348 if ( ! empty( $path ) ) {
1349 $doc_site .= ltrim( $path, '/' );
1350 }
1351
1352 $doc_url = trailingslashit( $doc_site ) . $utm_parameters;
1353
1354 if ( ! empty( $fragment ) ) {
1355 $doc_url .= '#' . $fragment;
1356 }
1357
1358 return $doc_url;
1359 }
1360
1361 /**
1362 * Sanitize CSS
1363 *
1364 * @param string $css Input
1365 *
1366 * @return string|string[] $css
1367 * @since 2.1
1368 */
1369 function sanitize_css( $css ) {
1370 $css = wp_strip_all_tags( $css );
1371
1372 if ( false !== strpos( $css, '<' ) ) {
1373 $css = preg_replace( '#<(\/?\w+)#', '\00003C$1', $css );
1374 }
1375
1376 return $css;
1377 }
1378
1379
1380 /**
1381 * Test if the current browser runs on a mobile device (smart phone, tablet, etc.)
1382 * Sort of custom version of wp_is_mobile
1383 */
1384 function powered_cache_is_mobile() {
1385
1386 global $powered_cache_mobile_browsers, $powered_cache_mobile_prefixes;
1387
1388 $mobile_browsers = addcslashes( implode( '|', preg_split( '/[\s*,\s*]*,+[\s*,\s*]*/', (string) $powered_cache_mobile_browsers ) ), ' ' );
1389 $mobile_prefixes = addcslashes( implode( '|', preg_split( '/[\s*,\s*]*,+[\s*,\s*]*/', (string) $powered_cache_mobile_prefixes ) ), ' ' );
1390
1391 if ( ! isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
1392 return false;
1393 }
1394
1395 $user_agent = wp_unslash( $_SERVER['HTTP_USER_AGENT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1396
1397 if ( ( preg_match( '#^.*(' . $mobile_browsers . ').*#i', $user_agent ) || preg_match( '#^(' . $mobile_prefixes . ').*#i', substr( $user_agent, 0, 4 ) ) ) ) {
1398 return true;
1399 }
1400
1401 return false;
1402 }
1403
1404 /**
1405 * If the site is a local site.
1406 *
1407 * @return bool
1408 * @since 2.2
1409 */
1410 function is_local_site() {
1411 $site_url = site_url();
1412 if ( ! is_string( $site_url ) ) {
1413 $site_url = '';
1414 }
1415
1416 // Check for localhost and sites using an IP only first.
1417 $is_local = $site_url && false === strpos( $site_url, '.' );
1418
1419 // Use Core's environment check, if available. Added in 5.5.0 / 5.5.1 (for `local` return value).
1420 if ( function_exists( 'wp_get_environment_type' ) && 'local' === wp_get_environment_type() ) {
1421 $is_local = true;
1422 }
1423
1424 // Then check for usual usual domains used by local dev tools.
1425 $known_local = array(
1426 '#\.local$#i',
1427 '#\.localhost$#i',
1428 '#\.test$#i',
1429 '#\.docksal$#i', // Docksal.
1430 '#\.docksal\.site$#i', // Docksal.
1431 '#\.dev\.cc$#i', // ServerPress.
1432 '#\.lndo\.site$#i', // Lando.
1433 );
1434
1435 if ( ! $is_local ) {
1436 foreach ( $known_local as $url ) {
1437 if ( preg_match( $url, $site_url ) ) {
1438 $is_local = true;
1439 break;
1440 }
1441 }
1442 }
1443
1444 /**
1445 * Filters is_local_site check.
1446 *
1447 * @param bool $is_local If the current site is a local site.
1448 *
1449 * @since 2.2
1450 */
1451 $is_local = apply_filters( 'powered_cache_is_local_site', $is_local );
1452
1453 return $is_local;
1454 }
1455
1456 /**
1457 * Check whether request for bypass or process normally
1458 *
1459 * @return bool
1460 * @since 3.0
1461 */
1462 function bypass_request() {
1463 if ( isset( $_GET['nopoweredcache'] ) && $_GET['nopoweredcache'] ) { // phpcs:ignore
1464 return true;
1465 }
1466
1467 return false;
1468 }
1469
1470
1471 /**
1472 * Calculate total amount of autoloaded data.
1473 *
1474 * @return int autoloaded data in bytes.
1475 * @global wpdb $wpdb WordPress database abstraction object.
1476 * @since 3.4
1477 */
1478 function autoloaded_options_size() {
1479 global $wpdb;
1480
1481 return (int) $wpdb->get_var( 'SELECT SUM(LENGTH(option_value)) FROM ' . $wpdb->prefix . 'options WHERE autoload = \'yes\'' ); // phpcs:ignore
1482 }
1483
1484 /**
1485 * Mask the string with asterisk
1486 *
1487 * @param string $input_string String
1488 * @param int $unmask_length The length of the string that will not be masked
1489 *
1490 * @return string
1491 * @since 3.4
1492 */
1493 function mask_string( $input_string, $unmask_length ) {
1494 $output_string = substr( $input_string, 0, $unmask_length );
1495
1496 if ( strlen( $input_string ) > $unmask_length ) {
1497 $output_string .= str_repeat( '*', strlen( $input_string ) - $unmask_length );
1498 }
1499
1500 return $output_string;
1501 }
1502
1503 /**
1504 * Get sensitive data in decrypted form
1505 *
1506 * @param string $field field name
1507 *
1508 * @return bool|mixed|string
1509 */
1510 function get_decrypted_setting( $field ) {
1511 $settings = \PoweredCache\Utils\get_settings();
1512 $value = isset( $settings[ $field ] ) ? $settings[ $field ] : '';
1513
1514 // decrypt the value
1515 $encryption = new Encryption();
1516 $decrypted_value = $encryption->decrypt( $value );
1517 if ( false !== $decrypted_value ) {
1518 return $decrypted_value;
1519 }
1520
1521 return $value;
1522 }
1523
1524
1525 /**
1526 * Check if a given IP is within a specific range.
1527 * Supports both IPv4 and IPv6 addresses.
1528 *
1529 * @param string $ip The IP address to check.
1530 * @param string $range The IP range in CIDR notation.
1531 *
1532 * @return bool True if the IP is in the range, false otherwise.
1533 */
1534 function is_ip_in_range( $ip, $range ) {
1535 if ( false !== strpos( $range, '/' ) ) {
1536 list( $subnet, $bits ) = explode( '/', $range, 2 );
1537 } else {
1538 $subnet = $range;
1539 $bits = ( false === filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) ? 32 : 128;
1540 }
1541
1542 $bits = intval( $bits );
1543
1544 if ( false !== filter_var( $subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
1545 $subnet_bin = inet_pton( $subnet );
1546 $ip_bin = inet_pton( $ip );
1547
1548 if ( false === $subnet_bin || false === $ip_bin ) {
1549 return false;
1550 }
1551
1552 $subnet_bin = str_pad( $subnet_bin, 16, "\0" );
1553 $ip_bin = str_pad( $ip_bin, 16, "\0" );
1554
1555 for ( $i = 0; $i * 8 < $bits; $i ++ ) {
1556 if ( $bits >= ( $i + 1 ) * 8 && $subnet_bin[ $i ] !== $ip_bin[ $i ] ) {
1557 return false;
1558 } elseif ( $bits > $i * 8 ) {
1559 $bitmask = 0xff00 >> ( $bits % 8 );
1560 if ( ( ord( $subnet_bin[ $i ] ) & $bitmask ) !== ( ord( $ip_bin[ $i ] ) & $bitmask ) ) {
1561 return false;
1562 }
1563 }
1564 }
1565
1566 return true;
1567 }
1568
1569 if ( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
1570 return false;
1571 }
1572
1573 $subnet_decimal = ip2long( $subnet );
1574 $ip_decimal = ip2long( $ip );
1575 $mask_decimal = - 1 << ( 32 - $bits );
1576
1577 return ( $subnet_decimal & $mask_decimal ) === ( $ip_decimal & $mask_decimal );
1578 }
1579
1580 /**
1581 * Check if the dev mode is active
1582 *
1583 * @return bool
1584 * @since 3.6
1585 */
1586 function is_dev_mode_active() {
1587 // Check global config first (fast)
1588 if ( ! empty( $GLOBALS['powered_cache_options']['dev_mode'] ) ) {
1589 return true;
1590 }
1591
1592 // Fallback to database option
1593 $settings = \PoweredCache\Utils\get_settings();
1594
1595 return ! empty( $settings['dev_mode'] );
1596 }
1597