PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / includes / class-wp-optimize-utils.php

class-wp-optimize-utils.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at includes/class-wp-optimize-utils.php

510 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('Access denied.');
4
5 if (!class_exists('WP_Optimize_Utils')) :
6
7 class WP_Optimize_Utils {
8
9 /**
10 * Returns the folder path for log files
11 *
12 * @return string - Folder path for log files with trailing slash
13 */
14 public static function get_log_folder_path() {
15 $upload_base = self::get_base_upload_dir();
16 if (!is_dir($upload_base . 'wpo/logs')) {
17 wp_mkdir_p($upload_base . 'wpo/logs');
18 }
19 // Ensure index.php in log folder to stop directory listing
20 if (!file_exists($upload_base . 'wpo/logs/index.php')) {
21 file_put_contents($upload_base . 'wpo/logs/index.php', "<?php");
22 }
23 return $upload_base . 'wpo/logs/';
24 }
25
26 /**
27 * Generates a log file name based on the given prefix.
28 *
29 * @param string $prefix The prefix to be added to the log file name.
30 * @return string The generated log file name.
31 */
32 public static function get_log_file_name($prefix) {
33 $secret = defined('AUTH_KEY') ? AUTH_KEY : 'WP_Optimize';
34 return $prefix . '-' . substr(md5($secret), 0, 20) . '.log';
35 }
36
37 /**
38 * Returns the file path for the log file.
39 *
40 * @param string $prefix The prefix to be added to the log file name.
41 * @return string The file path for the log file.
42 */
43 public static function get_log_file_path($prefix) {
44 return self::get_log_folder_path() . self::get_log_file_name($prefix);
45 }
46
47 /**
48 * Returns WordPress GMT offset in seconds.
49 *
50 * @return int
51 */
52 public static function get_gmt_offset() {
53 $timezone_string = get_option('timezone_string');
54
55 if (!empty($timezone_string)) {
56 $timezone = new DateTimeZone($timezone_string);
57 $gmt_offset = $timezone->getOffset(new DateTime());
58 } else {
59 $gmt_offset_option = (int) get_option('gmt_offset');
60 $gmt_offset = 3600 * $gmt_offset_option;
61 }
62
63 return $gmt_offset;
64 }
65
66 /**
67 * Returns the folder path for the upload directory with trailing slash
68 *
69 * @return string - Folder path for the upload directory with trailing slash
70 */
71 public static function get_base_upload_dir() {
72 $upload_dir = wp_upload_dir();
73 return trailingslashit($upload_dir['basedir']);
74 }
75
76 /**
77 * Returns file path relative to the `wp-content` directory
78 *
79 * @param string $path
80 * @return string
81 */
82 public static function get_wp_relative_path($path): string {
83 $path = wp_normalize_path($path);
84 $content_dir = wp_normalize_path(WP_CONTENT_DIR);
85
86 $pos = strpos($path, $content_dir);
87 if (false === $pos) {
88 return $path;
89 }
90
91 return substr($path, strlen($content_dir)+1) ?: '';
92 }
93
94 /**
95 * Get the file path
96 *
97 * @param string $url
98 * @return string
99 */
100 public static function get_file_path($url) {
101 $url_without_query = strtok($url, '?');
102 $url = false === $url_without_query ? $url : $url_without_query;
103
104 if (is_multisite()) {
105 if (function_exists('get_main_site_id')) {
106 $site_id = get_main_site_id();
107 } else {
108 $network = get_network();
109 $site_id = $network->site_id;
110 }
111 switch_to_blog($site_id);
112 }
113 $upload_dir = wp_upload_dir();
114 $uploads_url = trailingslashit($upload_dir['baseurl']);
115 $uploads_dir = trailingslashit($upload_dir['basedir']);
116 if (is_multisite()) {
117 restore_current_blog();
118 }
119 $possible_urls = array(
120 WP_CONTENT_URL => WP_CONTENT_DIR,
121 WP_PLUGIN_URL => WP_PLUGIN_DIR,
122 $uploads_url => $uploads_dir,
123 get_template_directory_uri() => get_template_directory(),
124 untrailingslashit(includes_url()) => ABSPATH . WPINC,
125 );
126 $file = '';
127 foreach ($possible_urls as $possible_url => $path) {
128 $pos = strpos($url, $possible_url);
129 if (0 === $pos) {
130 $file = substr_replace($url, $path, $pos, strlen($possible_url));
131 break;
132 }
133 }
134 return $file;
135 }
136
137 /**
138 * Returns folder staticstics - size and file count
139 *
140 * @param string $folder
141 * @return array
142 */
143 public static function get_folder_stats($folder, $files_to_ignore = array()) {
144 clearstatcache();
145
146 $size = 0;
147 $file_count = 0;
148
149 if (is_dir($folder)) {
150 try {
151 $dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS));
152
153 foreach ($dir as $file) {
154 if (!empty($files_to_ignore) && is_array($files_to_ignore) && in_array($file->getFilename(), $files_to_ignore)) continue;
155 try {
156 if ($file->isFile()) {
157 $size += $file->getSize();
158 $file_count++;
159 }
160 } catch (RuntimeException $e) {
161 error_log($e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Catching exception for debugging purpose
162 }
163 }
164 } catch (UnexpectedValueException $e) {
165 error_log($e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Catching exception for debugging purpose
166 }
167 }
168
169 return array(
170 'size' => $size,
171 'file_count' => $file_count,
172 );
173 }
174
175 /**
176 * Fetches the content of a remote file via HTTP request.
177 *
178 * @param string $url The URL of the remote file to retrieve.
179 * @param array $args Request arguments passed to wp_remote_get()
180 * @return string|false The content of the remote file on success, or false on failure.
181 */
182 public static function get_remote_file_content($url, $args = array()) {
183 $response = wp_safe_remote_get($url, $args);
184 if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) return false;
185
186 $body = wp_remote_retrieve_body($response);
187 if (empty($body)) return false;
188
189 return $body;
190 }
191
192 /**
193 * Parse tag attributes and return array with them.
194 *
195 * @param string $tag
196 * @return array
197 */
198 public static function parse_attributes($tag) {
199 $attributes = array();
200
201 $_attributes = wp_kses_hair($tag, wp_allowed_protocols());
202
203 if (empty($_attributes)) return $attributes;
204
205 foreach ($_attributes as $key => $value) {
206 $attributes[$key] = $value['value'];
207 }
208
209 return $attributes;
210 }
211
212 /**
213 * Checks whether supplied string is a valid html document or not
214 *
215 * @param string $html - HTML document as string
216 * @return bool
217 */
218 public static function is_valid_html($html) {
219 global $wp_query;
220
221 // is_feed() works only when $wp_query is set, and it raises a warning otherwise.
222 if (isset($wp_query) && is_feed()) return false;
223
224 // To prevent issue with `simple_html_dom` class
225 // Exit if it doesn't look like HTML
226 // https://github.com/rosell-dk/webp-express/issues/228
227 if (!preg_match("#^\\s*<#", $html)) return false;
228
229 if ('' === $html) return false;
230 return true;
231 }
232
233 /**
234 * Include simple html dom script if not available
235 */
236 public static function maybe_include_simple_html_dom() {
237 if (!function_exists('str_get_html')) {
238 require_once WPO_PLUGIN_MAIN_PATH . 'vendor/simplehtmldom/simplehtmldom/simple_html_dom.php';
239 }
240 }
241
242 /**
243 * Returns simplehtmldom\HtmlDocument object
244 *
245 * @param string $html_buffer - HTML document as string
246 * @return simplehtmldom\HtmlDocument | false
247 */
248 public static function get_simple_html_dom_object($html_buffer) {
249 self::maybe_include_simple_html_dom();
250 return str_get_html($html_buffer, false, false,
251 get_option('blog_charset'), false, DEFAULT_BR_TEXT,
252 DEFAULT_SPAN_TEXT, false);
253 }
254
255 /**
256 * Unserialize data
257 *
258 * @param string $serialized_data Data to be unserialized, should be one that is already serialized
259 * @param boolean|array $allowed_classes Either an array of class names which should be accepted, false to accept no classes, or true to accept all classes
260 * @param integer $max_depth The maximum depth of structures permitted during unserialization, and is intended to prevent stack overflows
261 * @return mixed Unserialized data can be any of types (integer, float, boolean, string, array or object)
262 */
263 public static function unserialize($serialized_data, $allowed_classes = false, $max_depth = 0) {
264 // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound -- Used in PHP 7.0+
265 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
266 return @unserialize(trim($serialized_data), array('allowed_classes' => $allowed_classes, 'max_depth' => $max_depth));
267 }
268
269 /**
270 * Checks whether supplied data is serialized or not, and if so, unserializes it
271 *
272 * @param string $serialized_data Data to be unserialized, should be one that is already serialized
273 * @param boolean|array $allowed_classes Either an array of class names which should be accepted, false to accept no classes, or true to accept all classes
274 * @param integer $max_depth The maximum depth of structures permitted during unserialization, and is intended to prevent stack overflows
275 * @return mixed Unserialized data can be any of types (integer, float, boolean, string, array or object)
276 */
277 public static function maybe_unserialize($serialized_data, $allowed_classes = false, $max_depth = 0) {
278 if (!is_serialized($serialized_data)) return $serialized_data;
279
280 return self::unserialize($serialized_data, $allowed_classes, $max_depth);
281 }
282
283 /**
284 * Get associative array with tag attributes and their values and build tag attribute string.
285 *
286 * @param array $attributes
287 * @return string
288 */
289 public static function build_attributes($attributes) {
290 $_attributes = array();
291
292 if (!empty($attributes)) {
293 foreach ($attributes as $key => $value) {
294 $_attributes[] = $key . '="' . esc_attr($value) . '"';
295 }
296 }
297
298 return join(' ', $_attributes);
299 }
300
301 /**
302 * Get user_agent for desktop or mobile used for different requests via wp_remote_get
303 *
304 * @param string $type
305 *
306 * @return string
307 */
308 public static function get_user_agent($type = 'desktop') {
309 global $wp_version;
310
311 $user_agent = sprintf(
312 'WP-Optimize/%s WordPress/%s (%s; %s)',
313 WPO_VERSION,
314 $wp_version,
315 ('mobile' === $type) ? 'Mobile' : 'Desktop',
316 home_url('/')
317 );
318
319 if ('gfont' === $type) {
320 // Custom UA returns TTF format. We use modern UA to get WOFF2 which is better for web
321 $user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
322 }
323
324 return apply_filters('wpo_user_agent', $user_agent, $type);
325 }
326
327 /**
328 * Builds a one-line summary string from debug backtrace.
329 *
330 * Intended for use in error_log() for identifying the caller's context.
331 * Example output: C:Test_Class|F:test_function()|L:100
332 *
333 * @return string
334 */
335 public static function get_backtrace_summary() {
336 $debug_backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Used for debugging purposes
337 $caller = $debug_backtrace[2] ?? array();
338
339 $class = $caller['class'] ?? 'N/A';
340 $function = $caller['function'] ?? 'N/A';
341 $line = $debug_backtrace[1]['line'] ?? 'N/A';
342
343 return sprintf('C:%s|F:%s()|L:%s', $class, $function, $line);
344 }
345
346 /**
347 * Check if the server is using HTTP/1.x
348 *
349 * @return bool
350 */
351 public static function is_request_protocol_http1(): bool {
352 $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_PROTOCOL'])) : '';
353 return stripos($protocol, 'HTTP/1.') !== false;
354 }
355
356 /**
357 * Wrapper for wp_delete_file() to ensure compatibility with WordPress versions prior to 6.1.
358 *
359 * In WordPress versions earlier than 6.1, wp_delete_file() returns void, in that case we check
360 * if the file is actually deleted or not to determine the boolean value to return
361 *
362 * @param string $file
363 * @return bool
364 */
365 public static function wp_delete_file($file) {
366 $wp_delete_file_result = wp_delete_file($file);
367
368 // when wp_delete_file() returns void we check if the file is deleted
369 if (null === $wp_delete_file_result) {
370 $wp_delete_file_result = false === is_file($file);
371 }
372
373 return $wp_delete_file_result;
374 }
375
376 /**
377 * Add UTM parameters to a URL and return the modified URL.
378 *
379 * @param string $url The original URL.
380 * @param array $params Optional UTM parameters.
381 * @param bool $override_url_params if true, it will override existing parameters in the url if matched.
382 *
383 * @return string Modified URL with UTM parameters added.
384 */
385 public static function add_utm_params($url, $params = array(), $override_url_params = false): string {
386 $default_params = array(
387 'utm_source' => 'wpo-plugin',
388 'utm_medium' => 'referral',
389 );
390
391 $utm_params = wp_parse_args($params, $default_params);
392
393 if ($override_url_params) {
394 return esc_url(add_query_arg($utm_params, $url));
395 }
396
397 $parsed = wp_parse_url($url, PHP_URL_QUERY);
398 $original_url_params = array();
399
400 if (!empty($parsed)) {
401 parse_str($parsed, $original_url_params);
402 }
403
404 foreach ($utm_params as $key => $value) {
405 if (isset($original_url_params[$key])) {
406 unset($utm_params[$key]);
407 }
408 }
409
410 return esc_url(add_query_arg($utm_params, $url));
411 }
412
413 /**
414 * Defines MAX_FILE_SIZE constant if not already defined.
415 *
416 * @return void
417 */
418 public static function define_maxfile_size_constant() {
419 $maxfile_size = apply_filters('wpo_max_file_size', 600000);
420 $maxfile_size = is_numeric($maxfile_size) && $maxfile_size > 0 ? (int) $maxfile_size : 600000;
421 if (!defined('MAX_FILE_SIZE')) {
422 define('MAX_FILE_SIZE', $maxfile_size);
423 }
424 }
425
426 /**
427 * Check if the given array contains all key-value pairs of another array.
428 *
429 * @param array $needle The array of key-value pairs to check for.
430 * @param array $haystack The array to check within.
431 * @return bool True if $haystack contains all key-value pairs of $needle, false otherwise.
432 */
433 public static function array_contains($needle, $haystack): bool {
434
435 if (!is_array($needle) || !is_array($haystack)) return false;
436
437 foreach ($needle as $key => $value) {
438 if (!array_key_exists($key, $haystack)) return false;
439
440 if (is_array($value)) {
441 if (!is_array($haystack[$key])) return false;
442 if (!self::array_contains($value, $haystack[$key])) return false;
443 } elseif ($haystack[$key] != $value) { // Loose comparison is intentional here to allow type juggling, e.g. '1' == 1 since we check sent form data with saved options where types can be different
444 return false;
445 }
446 }
447
448 return true;
449 }
450
451 /**
452 * Check if the current user has permission to purge cache.
453 *
454 * @return bool
455 */
456 public static function current_user_can_purge_cache() {
457 if (WP_Optimize::is_premium()) {
458 return WP_Optimize()->current_user_can() || WP_Optimize_Premium()->can_purge_the_cache();
459 } else {
460 return WP_Optimize()->current_user_can();
461 }
462 }
463
464 /**
465 * Get URL without purge cache-related parameters.
466 *
467 * @return string
468 */
469 public static function get_url_without_cache_purge_params() {
470 return remove_query_arg(self::get_cache_purge_params());
471 }
472
473 /**
474 * Outputs JavaScript to remove cache purge parameters from the URL.
475 *
476 * @return void
477 */
478 public static function script_to_remove_cache_purge_params_from_url() {
479 printf('<script>document.addEventListener("DOMContentLoaded", function() { var params = %s; var url = new URL(window.location.href); for (var param of params) { url.searchParams.delete(param); } history.replaceState({}, "", url); });</script>', json_encode(self::get_cache_purge_params()));
480 }
481
482 /**
483 * Returns the list of parameter names used for cache purge actions
484 *
485 * @return array
486 */
487 private static function get_cache_purge_params() {
488 $original_purge_params = array('_wpo_purge', 'wpo_minify_cache_purged', '_wpo_purge_minify_cache', 'wpo_all_pages_cache_purged', 'wpo_single_page_cache_purged');
489 $purge_params = apply_filters('wpo_get_cache_purge_params', $original_purge_params);
490
491 if (!is_array($purge_params)) $purge_params = $original_purge_params;
492
493 return $purge_params;
494 }
495
496 /**
497 * Returns a human-readable time difference or "Never" if no timestamp exists.
498 *
499 * @param int|null $timestamp Unix timestamp, or null/0 for "Never".
500 * @return string Human-readable time string.
501 */
502 public static function human_time_diff_or_never(?int $timestamp = null): string {
503 if (!$timestamp) return __('Never', 'wp-optimize');
504 /* translators: %s = human-readable time difference */
505 return sprintf(__('%s ago', 'wp-optimize'), human_time_diff($timestamp, time()));
506 }
507 }
508
509 endif;
510