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-browser-cache.php

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

559 lines 15.6 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('No direct access allowed');
4
5 /**
6 * Class WP_Optimize_Browser_Cache
7 */
8 class WP_Optimize_Browser_Cache {
9
10 use WP_Optimize_HTTP_Error_Codes_Trait;
11
12 private $_htaccess;
13
14 private $_options;
15
16 private $_wp_optimize;
17
18 /**
19 * Browser cache section in htaccess will be wrapped with this comment
20 *
21 * @var string
22 */
23 private $_htaccess_section_comment = 'WP-Optimize Browser Cache';
24
25 /**
26 * WP_Optimize_Browser_Cache constructor.
27 */
28 public function __construct() {
29 $this->_wp_optimize = WP_Optimize();
30
31 $this->_htaccess = new WP_Optimize_Htaccess();
32
33 $this->_options = $this->_wp_optimize->get_options();
34
35 $this->maybe_restore_expire_values_from_htaccess();
36 }
37
38 /**
39 * Returns singleton instance object
40 *
41 * @return WP_Optimize_Browser_Cache Returns `WP_Optimize_Browser_Cache` object
42 */
43 public static function instance() {
44 static $_instance = null;
45 if (null === $_instance) {
46 $_instance = new self();
47 }
48 return $_instance;
49 }
50
51 /**
52 * Check headers for Cache-Control and Etag. And if they are exist return true.
53 *
54 * @return bool|WP_Error
55 **/
56 public function is_enabled() {
57 $headers = $this->get_stylesheet_headers();
58
59 if (is_wp_error($headers)) return $headers;
60
61 $cache_control = isset($headers['cache-control']) ? (is_array($headers['cache-control']) ? implode(', ', $headers['cache-control']) : $headers['cache-control']) : '';
62 if (array_key_exists('expires', $headers) && array_key_exists('cache-control', $headers) && preg_match('/\b(public|private)\b/i', $cache_control)) {
63 $is_enabled = true;
64 } else {
65 $is_enabled = false;
66 }
67
68 if ($this->is_browser_cache_section_exists() && false === $this->_wp_optimize->is_apache_module_loaded(array('mod_expires', 'mod_headers'))) {
69 $is_enabled = new WP_Error('Browser cache', __('We successfully updated your .htaccess file.', 'wp-optimize') . ' ' . __('But it seems one of Apache modules - mod_expires or mod_headers is not active.', 'wp-optimize'));
70 }
71
72 return $is_enabled;
73 }
74
75 /**
76 * Get expire days and hours values from options.
77 *
78 * @return array // [0] - expire days, [1] - expire hours
79 */
80 private function get_expire_data() {
81 return array(
82 absint($this->_options->get_option('browser_cache_expire_days', 0)),
83 absint($this->_options->get_option('browser_cache_expire_hours', 0)),
84 );
85 }
86
87
88 /**
89 * Check if .htaccess has already updated settings (manually by user) then reset error message output
90 *
91 * @return void
92 */
93 public function maybe_reset_error_message_output() {
94 $error_message = $this->_options->get_option('browser_cache_error_message', '');
95 if ('' === $error_message) return;
96
97 list($expire_days, $expire_hours) = $this->get_expire_data();
98
99 $expire = $this->prepare_interval($expire_days, $expire_hours);
100 $search = 'ExpiresByType text/css "access '.$expire.'"';
101
102 // if we already have setting in the file then we can reset error message output
103 if (false !== strpos($this->_htaccess->get_content(), $search)) {
104 $this->_options->update_option('browser_cache_error_message', '');
105 $this->_options->update_option('browser_cache_output', '');
106 }
107 }
108
109 /**
110 * Set expire days and hours values.
111 *
112 * @param int $expire_days
113 * @param int $expire_hours
114 * @return void
115 */
116 private function set_expire_values($expire_days, $expire_hours) {
117 $this->_options->update_option('browser_cache_expire_days', absint($expire_days));
118 $this->_options->update_option('browser_cache_expire_hours', absint($expire_hours));
119 }
120
121 /**
122 * Enable browser cache - add settings into .htaccess.
123 *
124 * @return void
125 */
126 public function enable() {
127 list($expire_days, $expire_hours) = $this->get_expire_data();
128
129 $this->_htaccess->update_commented_section($this->prepare_browser_cache_section($expire_days, $expire_hours), $this->_htaccess_section_comment);
130 $this->_htaccess->write_file();
131 $this->_options->update_option('enable_browser_cache', true);
132 }
133
134 /**
135 * Disable cache - remove settings from .htaccess added in enable() function.
136 *
137 * @return void
138 */
139 public function disable() {
140 $this->_htaccess->remove_commented_section($this->_htaccess_section_comment);
141 $this->_htaccess->write_file();
142 $this->_options->update_option('enable_browser_cache', false);
143 }
144
145 /**
146 * Restore expiration settings from .htaccess if the options are empty.
147 *
148 * @return void
149 */
150 public function maybe_restore_expire_values_from_htaccess() {
151 $expire_days = $this->_options->get_option('browser_cache_expire_days', '');
152 $expire_hours = $this->_options->get_option('browser_cache_expire_hours', '');
153
154 if ('' === $expire_days && '' === $expire_hours) {
155 $this->restore_expire_values_from_htaccess();
156 }
157 }
158
159 /**
160 * Check if browser cache option is set to true then add section with browser cache settings into .htaccess.
161 *
162 * @return void
163 */
164 public function restore() {
165 list($expire_days, $expire_hours) = $this->get_expire_data();
166
167 $enabled = 0 !== $expire_days || 0 !== $expire_hours;
168
169 // If we don't have values in the database try to read them from .htaccess file
170 if (!$enabled) {
171 $this->restore_expire_values_from_htaccess();
172
173 list($expire_days, $expire_hours) = $this->get_expire_data();
174 $enabled = 0 !== $expire_days || 0 !== $expire_hours;
175 }
176
177 if ($enabled && $this->_htaccess->is_writable()) $this->enable();
178 }
179
180 /**
181 * Restore expire days and expire hours values form .htaccess if possible.
182 *
183 * @return void
184 */
185 private function restore_expire_values_from_htaccess() {
186 $htaccess_values = $this->get_browser_cache_section_expire_values();
187 list($expire_days, $expire_hours) = $htaccess_values;
188
189 $enabled = 0 !== $expire_days || 0 !== $expire_hours;
190
191 if ($enabled) {
192 $this->set_expire_values($expire_days, $expire_hours);
193 }
194 }
195
196 /**
197 * Check if section with browser cache settings already exists.
198 *
199 * @return bool
200 */
201 public function is_browser_cache_section_exists() {
202 return $this->_htaccess->is_commented_section_exists($this->_htaccess_section_comment);
203 }
204
205 /**
206 * Get expire days and hours values from the .htaccess file
207 *
208 * @return array|bool
209 */
210 private function get_browser_cache_section_expire_values() {
211 $section_content = $this->_htaccess->get_section_content();
212
213 if (preg_match('/ExpiresByType text\/css "access ([0-9a-z\s]+)"/i', $section_content, $matches)) {
214 return $this->expire_string_to_days_hours($matches[1]);
215 }
216
217 return array(0, 0);
218 }
219
220 /**
221 * Returns days and hours values from expire string
222 *
223 * @param string $string
224 * @return array
225 */
226 private function expire_string_to_days_hours($string) {
227 $days = 0;
228 $hours = 0;
229
230 preg_match_all(
231 '/(\d+)\s+(year|years|month|months|day|days|hour|hours)/i',
232 $string,
233 $matches,
234 PREG_SET_ORDER
235 );
236
237 foreach ($matches as $match) {
238 $value = (int) $match[1];
239 $unit = strtolower($match[2]);
240
241 switch ($unit) {
242 case 'year':
243 case 'years':
244 $days += $value * 365;
245 break;
246
247 case 'month':
248 case 'months':
249 $days += $value * 30;
250 break;
251
252 case 'day':
253 case 'days':
254 $days += $value;
255 break;
256
257 case 'hour':
258 case 'hours':
259 $hours += $value;
260 break;
261 }
262 }
263
264 // Convert extra hours into days
265 $days += intdiv($hours, 24);
266 $hours %= 24;
267
268 return array(absint($days), absint($hours));
269 }
270
271 /**
272 * Handle for enable_browser_cache command used in WP_Optimize_Commands.
273 *
274 * @param array $params - ['browser_cache_expire' => '1 month 15 days 2 hours' || '' - for disable cache]
275 * @return array
276 */
277 public function enable_browser_cache_command_handler(array $params): array {
278 $expire_days = absint($params['browser_cache_expire_days']);
279 $expire_hours = absint($params['browser_cache_expire_hours']);
280
281 list($current_expire_days, $current_expire_hours) = $this->get_browser_cache_section_expire_values();
282
283 // clear error message output
284 $this->_options->update_option('browser_cache_error_message', '');
285 $this->_options->update_option('browser_cache_output', '');
286
287 $enable = 0 !== $expire_days || 0 !== $expire_hours;
288
289 // Store new expire values in the database
290 $this->set_expire_values($expire_days, $expire_hours);
291
292 /**
293 * If we don't need to do anything in .htaccess then return message.
294 */
295 if ($enable === $this->_htaccess->is_commented_section_exists()
296 && $expire_days === $current_expire_days
297 && $expire_hours === $current_expire_hours
298 ) {
299 if ($enable) {
300 $message = __('Browser static caching settings already exists in the .htaccess file', 'wp-optimize');
301 } else {
302 $message = '';
303 }
304
305 return array(
306 'success' => true,
307 'enabled' => $enable,
308 'message' => $message,
309 );
310 }
311
312 $section_updated = $this->update_htaccess_section($enable);
313
314 $is_enabled = $this->is_enabled();
315
316 if ($section_updated) {
317
318 if (is_wp_error($is_enabled)) {
319 return array(
320 'success' => true,
321 'enabled' => false,
322 'error_message' => $is_enabled->get_error_message(),
323 );
324 } else {
325 return array(
326 'success' => true,
327 'enabled' => $is_enabled,
328 'message' => __('We successfully updated your .htaccess file.', 'wp-optimize'),
329 );
330 }
331 } else {
332
333 if ($enable) {
334 // translators: %s is a file name
335 $message = sprintf(__("We can't update your %s file.", 'wp-optimize'), $this->_htaccess->get_filename()) . ' ' . __('Please try to add following lines manually:', 'wp-optimize');
336 } else {
337 // translators: %s is a file name
338 $message = sprintf(__("We can't update your %s file.", 'wp-optimize'), $this->_htaccess->get_filename()) . ' ' . __('Please try to remove following lines manually:', 'wp-optimize');
339 }
340
341 $output = htmlentities($this->get_htaccess_section_output($enable));
342
343 // store error message output
344 $this->_options->update_option('browser_cache_error_message', $message);
345 $this->_options->update_option('browser_cache_output', $output);
346
347 if (is_wp_error($is_enabled)) {
348 $message .= ' ' .$is_enabled->get_error_message();
349 $is_enabled = false;
350 }
351
352 return array(
353 'success' => false,
354 'enabled' => $is_enabled,
355 'error_message' => $message,
356 'output' => $output,
357 );
358 }
359 }
360
361 /**
362 * Update the .htaccess section for browser caching.
363 *
364 * @param boolean $enable
365 * @return bool True if section updated successfully, false otherwise
366 */
367 private function update_htaccess_section(bool $enable) {
368
369 if (!$this->_htaccess->is_writable()) {
370 return false;
371 }
372
373 // update commented section
374 if ($enable) {
375 $this->enable();
376 } else {
377 $this->disable();
378 }
379
380 // read updated file.
381 $this->_htaccess->read_file();
382 // check if section added or removed successfully.
383 $section_exists = $this->_htaccess->is_commented_section_exists();
384 // return true if section updated successfully, false otherwise.
385 return $enable === $section_exists;
386 }
387
388 /**
389 * Get output content for enable/disable browser cache in .htaccess.
390 *
391 * @param bool $enable
392 * @return string
393 */
394 private function get_htaccess_section_output($enable) {
395 list($expire_days, $expire_hours) = $this->get_expire_data();
396
397 if ($enable) {
398 $cache_section = $this->prepare_browser_cache_section($expire_days, $expire_hours);
399
400 $output = $this->_htaccess->get_section_begin_comment() . PHP_EOL .
401 join(PHP_EOL, $this->_htaccess->get_flat_array($cache_section)).
402 PHP_EOL . $this->_htaccess->get_section_end_comment();
403 } else {
404 $output = $this->_htaccess->get_section_begin_comment() . PHP_EOL .
405 ' ... ... ... '.
406 PHP_EOL . $this->_htaccess->get_section_end_comment();
407 }
408
409 return $output;
410 }
411
412 /**
413 * Use $days and $hours values to build correct time interval as a string like '2 days 3 hours' or empty string if date is empty.
414 *
415 * @param int $days
416 * @param int $hours
417 * @return string
418 */
419 private function prepare_interval(int $days, int $hours): string {
420
421 if (0 === $days && 0 === $hours) {
422 return '';
423 }
424
425 $parts = array();
426
427 // if hours value more than one day then fix it.
428 $days += floor($hours / 24);
429 $hours = $hours % 24;
430
431 $years = floor($days / 365);
432 $days = $days % 365;
433 $months = floor($days / 30);
434 $days = $days % 30;
435
436 if ($years > 0) {
437 $parts[] = $years . ($years > 1 ? ' years' : ' year');
438 }
439
440 if ($months > 0) {
441 $parts[] = $months . ($months > 1 ? ' months' : ' month');
442 }
443
444 if ($days > 0) {
445 $parts[] = $days . ($days > 1 ? ' days' : ' day');
446 }
447
448 if ($hours > 0) {
449 $parts[] = $hours . ($hours > 1 ? ' hours' : ' hour');
450 }
451
452 return join(' ', $parts);
453 }
454
455 /**
456 * Converts a human-readable time string into total seconds
457 *
458 * @param int $days
459 * @param int $hours
460 * @return string Total number of seconds
461 */
462 private function convert_to_seconds(int $days, int $hours): string {
463 // Split the string into parts
464 $total_seconds = $days * 24 * 60 * 60 + $hours * 60 * 60;
465
466 return strval($total_seconds);
467 }
468
469 /**
470 * Build browser cache section array.
471 *
472 * @param int $expire_days
473 * @param int $expire_hours
474 * @return array
475 */
476 public function prepare_browser_cache_section(int $expire_days, int $expire_hours): array {
477 $expire = $this->prepare_interval($expire_days, $expire_hours);
478 $max_age = $this->convert_to_seconds($expire_days, $expire_hours);
479
480 return array(
481 array(
482 '<IfModule mod_setenvif.c>',
483 'SetEnvIf Request_URI "/wp-admin/" WPO_NO_CACHE',
484 '</IfModule>',
485 ),
486 array(
487 '<IfModule mod_expires.c>',
488 'ExpiresActive On',
489 'ExpiresByType text/css "access '.$expire.'"',
490 'ExpiresByType image/gif "access '.$expire.'"',
491 'ExpiresByType image/png "access '.$expire.'"',
492 'ExpiresByType image/jpg "access '.$expire.'"',
493 'ExpiresByType image/jpeg "access '.$expire.'"',
494 'ExpiresByType image/webp "access '.$expire.'"',
495 'ExpiresByType image/x-icon "access '.$expire.'"',
496 'ExpiresByType application/pdf "access '.$expire.'"',
497 'ExpiresByType application/javascript "access '.$expire.'"',
498 'ExpiresByType text/x-javascript "access '.$expire.'"',
499 'ExpiresByType application/x-shockwave-flash "access '.$expire.'"',
500 'ExpiresByType image/svg+xml "access '.$expire.'"',
501 'ExpiresByType font/woff "access '.$expire.'"',
502 'ExpiresByType font/woff2 "access '.$expire.'"',
503 'ExpiresByType font/x-woff "access '.$expire.'"',
504 'ExpiresByType application/font-woff "access '.$expire.'"',
505 'ExpiresByType application/x-font-woff "access '.$expire.'"',
506 'ExpiresByType font/ttf "access '.$expire.'"',
507 'ExpiresByType font/otf "access '.$expire.'"',
508 'ExpiresByType application/vnd.ms-fontobject "access '.$expire.'"',
509 'ExpiresByType application/json "access 0 seconds"',
510 'ExpiresByType application/xml "access 0 seconds"',
511 '</IfModule>',
512 ),
513 '',
514 array(
515 '<IfModule mod_headers.c>',
516 array(
517 '<FilesMatch "\.(ico|jpe?g|png|gif|webp|swf|svg)$">',
518 'Header set Cache-Control "public, max-age='.$max_age.'" env=!WPO_NO_CACHE',
519 '</FilesMatch>',
520 ),
521 array(
522 '<FilesMatch "\.(css)$">',
523 'Header set Cache-Control "public, max-age='.$max_age.'" env=!WPO_NO_CACHE',
524 '</FilesMatch>',
525 ),
526 array(
527 '<FilesMatch "\.(js)$">',
528 'Header set Cache-Control "private, max-age='.$max_age.'" env=!WPO_NO_CACHE',
529 '</FilesMatch>',
530 ),
531 array(
532 '<FilesMatch "\.(woff2?|ttf|otf|eot)$">',
533 'Header set Cache-Control "public, max-age='.$max_age.'" env=!WPO_NO_CACHE',
534 '</FilesMatch>',
535 ),
536 array(
537 '<FilesMatch "\.(x?html?)$">',
538 'Header set Cache-Control "private, must-revalidate, max-age='.$max_age.'" env=!WPO_NO_CACHE',
539 '</FilesMatch>',
540 ),
541 array(
542 '<FilesMatch "\.php$">',
543 'Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"',
544 '</FilesMatch>',
545 ),
546 '</IfModule>',
547 ),
548 array(
549 '<IfModule mod_headers.c>',
550 'Header unset Expires env=WPO_NO_CACHE',
551 '</IfModule>',
552 ),
553 '',
554 '#Disable ETag',
555 'FileETag None',
556 );
557 }
558 }
559