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
← All changes | includes/class-wp-optimize-utils.php +470 -5 3.3.2 → 4.7.0 View file →
@@ -11,16 +11,19 @@
11 11 *
12 12 * @return string - Folder path for log files with trailing slash
13 13 */
14 14 public static function get_log_folder_path() {
15 - $upload_dir = wp_upload_dir();
16 - $upload_base = trailingslashit($upload_dir['basedir']);
15 + $upload_base = self::get_base_upload_dir();
17 16 if (!is_dir($upload_base . 'wpo/logs')) {
18 17 wp_mkdir_p($upload_base . 'wpo/logs');
19 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 + }
20 23 return $upload_base . 'wpo/logs/';
21 24 }
22 -
25 +
23 26 /**
24 27 * Generates a log file name based on the given prefix.
25 28 *
26 29 * @param string $prefix The prefix to be added to the log file name.
@@ -26,11 +29,12 @@
26 29 * @param string $prefix The prefix to be added to the log file name.
27 30 * @return string The generated log file name.
28 31 */
29 32 public static function get_log_file_name($prefix) {
30 - return $prefix . '-' . substr(md5(wp_salt()), 0, 20) . '.log';
33 + $secret = defined('AUTH_KEY') ? AUTH_KEY : 'WP_Optimize';
34 + return $prefix . '-' . substr(md5($secret), 0, 20) . '.log';
31 35 }
32 -
36 +
33 37 /**
34 38 * Returns the file path for the log file.
35 39 *
36 40 * @param string $prefix The prefix to be added to the log file name.
@@ -37,8 +41,469 @@
37 41 * @return string The file path for the log file.
38 42 */
39 43 public static function get_log_file_path($prefix) {
40 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()));
41 506 }
42 507 }
43 508
44 509 endif;