PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.5.4
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.5.4
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.5.4, at includes/class-wp-optimize-utils.php

437 lines 13.3 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 if (is_multisite()) {
102 if (function_exists('get_main_site_id')) {
103 $site_id = get_main_site_id();
104 } else {
105 $network = get_network();
106 $site_id = $network->site_id;
107 }
108 switch_to_blog($site_id);
109 }
110 $upload_dir = wp_upload_dir();
111 $uploads_url = trailingslashit($upload_dir['baseurl']);
112 $uploads_dir = trailingslashit($upload_dir['basedir']);
113 if (is_multisite()) {
114 restore_current_blog();
115 }
116 $possible_urls = array(
117 WP_CONTENT_URL => WP_CONTENT_DIR,
118 WP_PLUGIN_URL => WP_PLUGIN_DIR,
119 $uploads_url => $uploads_dir,
120 get_template_directory_uri() => get_template_directory(),
121 untrailingslashit(includes_url()) => ABSPATH . WPINC,
122 );
123 $file = '';
124 foreach ($possible_urls as $possible_url => $path) {
125 $pos = strpos($url, $possible_url);
126 if (0 === $pos) {
127 $file = substr_replace($url, $path, $pos, strlen($possible_url));
128 break;
129 }
130 }
131 return $file;
132 }
133
134 /**
135 * Returns folder staticstics - size and file count
136 *
137 * @param string $folder
138 * @return array
139 */
140 public static function get_folder_stats($folder, $files_to_ignore = array()) {
141 clearstatcache();
142
143 $size = 0;
144 $file_count = 0;
145
146 if (is_dir($folder)) {
147 try {
148 $dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS));
149
150 foreach ($dir as $file) {
151 if (!empty($files_to_ignore) && is_array($files_to_ignore) && in_array($file->getFilename(), $files_to_ignore)) continue;
152 try {
153 if ($file->isFile()) {
154 $size += $file->getSize();
155 $file_count++;
156 }
157 } catch (RuntimeException $e) {
158 error_log($e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Catching exception for debugging purpose
159 }
160 }
161 } catch (UnexpectedValueException $e) {
162 error_log($e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Catching exception for debugging purpose
163 }
164 }
165
166 return array(
167 'size' => $size,
168 'file_count' => $file_count,
169 );
170 }
171
172 /**
173 * Fetches the content of a remote file via HTTP request.
174 *
175 * @param string $url The URL of the remote file to retrieve.
176 * @param array $args Request arguments passed to wp_remote_get()
177 * @return string|false The content of the remote file on success, or false on failure.
178 */
179 public static function get_remote_file_content($url, $args = array()) {
180 $response = wp_safe_remote_get($url, $args);
181 if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) return false;
182
183 $body = wp_remote_retrieve_body($response);
184 if (empty($body)) return false;
185
186 return $body;
187 }
188
189 /**
190 * Parse tag attributes and return array with them.
191 *
192 * @param string $tag
193 * @return array
194 */
195 public static function parse_attributes($tag) {
196 $attributes = array();
197
198 $_attributes = wp_kses_hair($tag, wp_allowed_protocols());
199
200 if (empty($_attributes)) return $attributes;
201
202 foreach ($_attributes as $key => $value) {
203 $attributes[$key] = $value['value'];
204 }
205
206 return $attributes;
207 }
208
209 /**
210 * Checks whether supplied string is a valid html document or not
211 *
212 * @param string $html - HTML document as string
213 * @return bool
214 */
215 public static function is_valid_html($html) {
216 global $wp_query;
217
218 // is_feed() works only when $wp_query is set, and it raises a warning otherwise.
219 if (isset($wp_query) && is_feed()) return false;
220
221 // To prevent issue with `simple_html_dom` class
222 // Exit if it doesn't look like HTML
223 // https://github.com/rosell-dk/webp-express/issues/228
224 if (!preg_match("#^\\s*<#", $html)) return false;
225
226 if ('' === $html) return false;
227 return true;
228 }
229
230 /**
231 * Include simple html dom script if not available
232 */
233 public static function maybe_include_simple_html_dom() {
234 if (!function_exists('str_get_html')) {
235 require_once WPO_PLUGIN_MAIN_PATH . 'vendor/simplehtmldom/simplehtmldom/simple_html_dom.php';
236 }
237 }
238
239 /**
240 * Returns simplehtmldom\HtmlDocument object
241 *
242 * @param string $html_buffer - HTML document as string
243 * @return simplehtmldom\HtmlDocument | false
244 */
245 public static function get_simple_html_dom_object($html_buffer) {
246 self::maybe_include_simple_html_dom();
247 return str_get_html($html_buffer, false, false,
248 get_option('blog_charset'), false, DEFAULT_BR_TEXT,
249 DEFAULT_SPAN_TEXT, false);
250 }
251
252 /**
253 * Unserialize data
254 *
255 * @param string $serialized_data Data to be unserialized, should be one that is already serialized
256 * @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
257 * @param integer $max_depth The maximum depth of structures permitted during unserialization, and is intended to prevent stack overflows
258 * @return mixed Unserialized data can be any of types (integer, float, boolean, string, array or object)
259 */
260 public static function unserialize($serialized_data, $allowed_classes = false, $max_depth = 0) {
261 // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.unserialize_optionsFound -- Used in PHP 7.0+
262 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- suppress PHP warning in case of failure
263 return @unserialize(trim($serialized_data), array('allowed_classes' => $allowed_classes, 'max_depth' => $max_depth));
264 }
265
266 /**
267 * Checks whether supplied data is serialized or not, and if so, unserializes it
268 *
269 * @param string $serialized_data Data to be unserialized, should be one that is already serialized
270 * @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
271 * @param integer $max_depth The maximum depth of structures permitted during unserialization, and is intended to prevent stack overflows
272 * @return mixed Unserialized data can be any of types (integer, float, boolean, string, array or object)
273 */
274 public static function maybe_unserialize($serialized_data, $allowed_classes = false, $max_depth = 0) {
275 if (!is_serialized($serialized_data)) return $serialized_data;
276
277 return self::unserialize($serialized_data, $allowed_classes, $max_depth);
278 }
279
280 /**
281 * Get associative array with tag attributes and their values and build tag attribute string.
282 *
283 * @param array $attributes
284 * @return string
285 */
286 public static function build_attributes($attributes) {
287 $_attributes = array();
288
289 if (!empty($attributes)) {
290 foreach ($attributes as $key => $value) {
291 $_attributes[] = $key . '="' . esc_attr($value) . '"';
292 }
293 }
294
295 return join(' ', $_attributes);
296 }
297
298 /**
299 * Get user_agent for desktop or mobile used for different requests via wp_remote_get
300 *
301 * @param string $type
302 *
303 * @return string
304 */
305 public static function get_user_agent($type = 'desktop') {
306 global $wp_version;
307
308 $user_agent = sprintf(
309 'WP-Optimize/%s WordPress/%s (%s; %s)',
310 WPO_VERSION,
311 $wp_version,
312 ('mobile' === $type) ? 'Mobile' : 'Desktop',
313 home_url('/')
314 );
315
316 if ('gfont' === $type) {
317 // Custom UA returns TTF format. We use modern UA to get WOFF2 which is better for web
318 $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';
319 }
320
321 return apply_filters('wpo_user_agent', $user_agent, $type);
322 }
323
324 /**
325 * Builds a one-line summary string from debug backtrace.
326 *
327 * Intended for use in error_log() for identifying the caller's context.
328 * Example output: C:Test_Class|F:test_function()|L:100
329 *
330 * @return string
331 */
332 public static function get_backtrace_summary() {
333 $debug_backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace -- Used for debugging purposes
334 $caller = $debug_backtrace[2] ?? array();
335
336 $class = $caller['class'] ?? 'N/A';
337 $function = $caller['function'] ?? 'N/A';
338 $line = $debug_backtrace[1]['line'] ?? 'N/A';
339
340 return sprintf('C:%s|F:%s()|L:%s', $class, $function, $line);
341 }
342
343 /**
344 * Check if the server is using HTTP/1.x
345 *
346 * @return bool
347 */
348 public static function is_request_protocol_http1(): bool {
349 $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_PROTOCOL'])) : '';
350 return stripos($protocol, 'HTTP/1.') !== false;
351 }
352
353 /**
354 * Wrapper for wp_delete_file() to ensure compatibility with WordPress versions prior to 6.1.
355 *
356 * In WordPress versions earlier than 6.1, wp_delete_file() returns void, in that case we check
357 * if the file is actually deleted or not to determine the boolean value to return
358 *
359 * @param string $file
360 * @return bool
361 */
362 public static function wp_delete_file($file) {
363 $wp_delete_file_result = wp_delete_file($file);
364
365 // when wp_delete_file() returns void we check if the file is deleted
366 if (null === $wp_delete_file_result) {
367 $wp_delete_file_result = false === is_file($file);
368 }
369
370 return $wp_delete_file_result;
371 }
372
373 /**
374 * Add UTM parameters to a URL and return the modified URL.
375 *
376 * @param string $url The original URL.
377 * @param array $params Optional UTM parameters.
378 * @param bool $override_url_params if true, it will override existing parameters in the url if matched.
379 *
380 * @return string Modified URL with UTM parameters added.
381 */
382 public static function add_utm_params($url, $params = array(), $override_url_params = false): string {
383 $default_params = array(
384 'utm_source' => 'wpo-plugin',
385 'utm_medium' => 'referral',
386 );
387
388 $utm_params = wp_parse_args($params, $default_params);
389
390 if ($override_url_params) {
391 return esc_url(add_query_arg($utm_params, $url));
392 }
393
394 $parsed = wp_parse_url($url, PHP_URL_QUERY);
395 $original_url_params = array();
396
397 if (!empty($parsed)) {
398 parse_str($parsed, $original_url_params);
399 }
400
401 foreach ($utm_params as $key => $value) {
402 if (isset($original_url_params[$key])) {
403 unset($utm_params[$key]);
404 }
405 }
406
407 return esc_url(add_query_arg($utm_params, $url));
408 }
409
410 /**
411 * Check if the given array contains all key-value pairs of another array.
412 *
413 * @param array $needle The array of key-value pairs to check for.
414 * @param array $haystack The array to check within.
415 * @return bool True if $haystack contains all key-value pairs of $needle, false otherwise.
416 */
417 public static function array_contains($needle, $haystack): bool {
418
419 if (!is_array($needle) || !is_array($haystack)) return false;
420
421 foreach ($needle as $key => $value) {
422 if (!array_key_exists($key, $haystack)) return false;
423
424 if (is_array($value)) {
425 if (!is_array($haystack[$key])) return false;
426 if (!self::array_contains($value, $haystack[$key])) return false;
427 } 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
428 return false;
429 }
430 }
431
432 return true;
433 }
434 }
435
436 endif;
437