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 +342 -5 3.7.1 → 4.7.0 View file →
@@ -15,8 +15,12 @@
15 15 $upload_base = self::get_base_upload_dir();
16 16 if (!is_dir($upload_base . 'wpo/logs')) {
17 17 wp_mkdir_p($upload_base . 'wpo/logs');
18 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 + }
19 23 return $upload_base . 'wpo/logs/';
20 24 }
21 25
22 26 /**
@@ -25,9 +29,10 @@
25 29 * @param string $prefix The prefix to be added to the log file name.
26 30 * @return string The generated log file name.
27 31 */
28 32 public static function get_log_file_name($prefix) {
29 - 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';
30 35 }
31 36
32 37 /**
33 38 * Returns the file path for the log file.
@@ -50,10 +55,10 @@
50 55 if (!empty($timezone_string)) {
51 56 $timezone = new DateTimeZone($timezone_string);
52 57 $gmt_offset = $timezone->getOffset(new DateTime());
53 58 } else {
54 - $gmt_offset_option = get_option('gmt_offset');
55 - $gmt_offset = (int) (3600 * $gmt_offset_option);
59 + $gmt_offset_option = (int) get_option('gmt_offset');
60 + $gmt_offset = 3600 * $gmt_offset_option;
56 61 }
57 62
58 63 return $gmt_offset;
59 64 }
@@ -68,8 +73,26 @@
68 73 return trailingslashit($upload_dir['basedir']);
69 74 }
70 75
71 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 + /**
72 95 * Get the file path
73 96 *
74 97 * @param string $url
75 98 * @return string
@@ -74,8 +97,11 @@
74 97 * @param string $url
75 98 * @return string
76 99 */
77 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 +
78 104 if (is_multisite()) {
79 105 if (function_exists('get_main_site_id')) {
80 106 $site_id = get_main_site_id();
81 107 } else {
@@ -108,8 +134,63 @@
108 134 return $file;
109 135 }
110 136
111 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 + /**
112 193 * Parse tag attributes and return array with them.
113 194 *
114 195 * @param string $tag
115 196 * @return array
@@ -134,9 +215,12 @@
134 215 * @param string $html - HTML document as string
135 216 * @return bool
136 217 */
137 218 public static function is_valid_html($html) {
138 - if (is_feed()) return false;
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;
139 223
140 224 // To prevent issue with `simple_html_dom` class
141 225 // Exit if it doesn't look like HTML
142 226 // https://github.com/rosell-dk/webp-express/issues/228
@@ -158,9 +242,9 @@
158 242 /**
159 243 * Returns simplehtmldom\HtmlDocument object
160 244 *
161 245 * @param string $html_buffer - HTML document as string
162 - * @return simplehtmldom\HtmlDocument | boolean
246 + * @return simplehtmldom\HtmlDocument | false
163 247 */
164 248 public static function get_simple_html_dom_object($html_buffer) {
165 249 self::maybe_include_simple_html_dom();
166 250 return str_get_html($html_buffer, false, false,
@@ -165,8 +249,261 @@
165 249 self::maybe_include_simple_html_dom();
166 250 return str_get_html($html_buffer, false, false,
167 251 get_option('blog_charset'), false, DEFAULT_BR_TEXT,
168 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()));
169 506 }
170 507 }
171 508
172 509 endif;